aboutsummaryrefslogtreecommitdiffhomepage
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/e2e/src/po/my-account.ts72
-rw-r--r--client/e2e/src/po/video-update.po.ts20
-rw-r--r--client/e2e/src/po/video-watch.po.ts43
-rw-r--r--client/e2e/src/videos.e2e-spec.ts159
-rw-r--r--client/proxy.config.json5
-rw-r--r--client/src/app/+about/about-follows/about-follows.component.html22
-rw-r--r--client/src/app/+about/about-follows/about-follows.component.scss14
-rw-r--r--client/src/app/+about/about-follows/about-follows.component.ts103
-rw-r--r--client/src/app/+about/about-routing.module.ts10
-rw-r--r--client/src/app/+about/about.component.html4
-rw-r--r--client/src/app/+about/about.module.ts2
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.html36
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.scss36
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.ts71
-rw-r--r--client/src/app/+accounts/account-videos/account-videos.component.ts4
-rw-r--r--client/src/app/+accounts/accounts-routing.module.ts6
-rw-r--r--client/src/app/+accounts/accounts.component.html4
-rw-r--r--client/src/app/+admin/admin.module.ts3
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html12
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts36
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.ts2
-rw-r--r--client/src/app/+admin/follows/following-add/following-add.component.ts2
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.ts2
-rw-r--r--client/src/app/+admin/follows/index.ts1
-rw-r--r--client/src/app/+admin/follows/shared/index.ts1
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.ts5
-rw-r--r--client/src/app/+my-account/my-account-history/my-account-history.component.ts2
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-change-email/index.ts1
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.html36
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.scss24
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.ts73
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.html2
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts2
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-settings.component.html3
-rw-r--r--client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.html7
-rw-r--r--client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts5
-rw-r--r--client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts15
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component.ts3
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.html10
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts4
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component.ts47
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.html2
-rw-r--r--client/src/app/+my-account/my-account.module.ts4
-rw-r--r--client/src/app/+signup/+register/custom-stepper.component.html25
-rw-r--r--client/src/app/+signup/+register/custom-stepper.component.scss66
-rw-r--r--client/src/app/+signup/+register/custom-stepper.component.ts19
-rw-r--r--client/src/app/+signup/+register/register-routing.module.ts (renamed from client/src/app/signup/signup-routing.module.ts)17
-rw-r--r--client/src/app/+signup/+register/register-step-channel.component.html54
-rw-r--r--client/src/app/+signup/+register/register-step-channel.component.ts56
-rw-r--r--client/src/app/+signup/+register/register-step-user.component.html73
-rw-r--r--client/src/app/+signup/+register/register-step-user.component.ts54
-rw-r--r--client/src/app/+signup/+register/register.component.html47
-rw-r--r--client/src/app/+signup/+register/register.component.scss81
-rw-r--r--client/src/app/+signup/+register/register.component.ts89
-rw-r--r--client/src/app/+signup/+register/register.module.ts33
-rw-r--r--client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html (renamed from client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html)0
-rw-r--r--client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss (renamed from client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss)0
-rw-r--r--client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts (renamed from client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts)0
-rw-r--r--client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html18
-rw-r--r--client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts (renamed from client/src/app/+verify-account/verify-account-email/verify-account-email.component.ts)22
-rw-r--r--client/src/app/+signup/+verify-account/verify-account-routing.module.ts (renamed from client/src/app/+verify-account/verify-account-routing.module.ts)8
-rw-r--r--client/src/app/+signup/+verify-account/verify-account.module.ts25
-rw-r--r--client/src/app/+signup/shared/signup-shared.module.ts21
-rw-r--r--client/src/app/+signup/shared/signup-success.component.html16
-rw-r--r--client/src/app/+signup/shared/signup-success.component.scss76
-rw-r--r--client/src/app/+signup/shared/signup-success.component.ts10
-rw-r--r--client/src/app/+verify-account/index.ts2
-rw-r--r--client/src/app/+verify-account/verify-account-email/verify-account-email.component.html15
-rw-r--r--client/src/app/+verify-account/verify-account.module.ts27
-rw-r--r--client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts5
-rw-r--r--client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts2
-rw-r--r--client/src/app/app-routing.module.ts6
-rw-r--r--client/src/app/app.module.ts2
-rw-r--r--client/src/app/core/core.module.ts3
-rw-r--r--client/src/app/core/routing/redirect.service.ts9
-rw-r--r--client/src/app/core/routing/unlogged-guard.service.ts25
-rw-r--r--client/src/app/core/server/server.service.ts13
-rw-r--r--client/src/app/menu/menu.component.html2
-rw-r--r--client/src/app/search/search.component.html2
-rw-r--r--client/src/app/search/search.component.scss12
-rw-r--r--client/src/app/search/search.module.ts5
-rw-r--r--client/src/app/shared/actor/actor.model.ts2
-rw-r--r--client/src/app/shared/buttons/button.component.scss13
-rw-r--r--client/src/app/shared/buttons/button.component.ts2
-rw-r--r--client/src/app/shared/buttons/delete-button.component.html2
-rw-r--r--client/src/app/shared/buttons/edit-button.component.html2
-rw-r--r--client/src/app/shared/forms/form-validators/user-validators.service.ts33
-rw-r--r--client/src/app/shared/forms/peertube-checkbox.component.scss3
-rw-r--r--client/src/app/shared/forms/reactive-file.component.html7
-rw-r--r--client/src/app/shared/forms/reactive-file.component.scss10
-rw-r--r--client/src/app/shared/forms/reactive-file.component.ts2
-rw-r--r--client/src/app/shared/images/image-upload.component.html9
-rw-r--r--client/src/app/shared/images/image-upload.component.scss18
-rw-r--r--client/src/app/shared/images/preview-upload.component.html13
-rw-r--r--client/src/app/shared/images/preview-upload.component.scss27
-rw-r--r--client/src/app/shared/images/preview-upload.component.ts (renamed from client/src/app/shared/images/image-upload.component.ts)17
-rw-r--r--client/src/app/shared/instance/follow.service.ts (renamed from client/src/app/+admin/follows/shared/follow.service.ts)8
-rw-r--r--client/src/app/shared/misc/loader.component.html2
-rw-r--r--client/src/app/shared/misc/loader.component.scss14
-rw-r--r--client/src/app/shared/shared.module.ts20
-rw-r--r--client/src/app/shared/users/user-notifications.component.scss1
-rw-r--r--client/src/app/shared/users/user.model.ts1
-rw-r--r--client/src/app/shared/users/user.service.ts38
-rw-r--r--client/src/app/shared/video-channel/video-channel.service.ts17
-rw-r--r--client/src/app/shared/video/abstract-video-list.html20
-rw-r--r--client/src/app/shared/video/abstract-video-list.scss36
-rw-r--r--client/src/app/shared/video/abstract-video-list.ts80
-rw-r--r--client/src/app/shared/video/modals/video-download.component.html5
-rw-r--r--client/src/app/shared/video/modals/video-download.component.ts13
-rw-r--r--client/src/app/shared/video/video-details.model.ts3
-rw-r--r--client/src/app/shared/video/video-edit.model.ts5
-rw-r--r--client/src/app/shared/video/video.model.ts2
-rw-r--r--client/src/app/shared/video/videos-selection.component.ts2
-rw-r--r--client/src/app/signup/index.ts3
-rw-r--r--client/src/app/signup/signup.component.html72
-rw-r--r--client/src/app/signup/signup.component.scss39
-rw-r--r--client/src/app/signup/signup.component.ts78
-rw-r--r--client/src/app/signup/signup.module.ts24
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.html14
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.ts8
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html2
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts2
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html2
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts2
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-send.ts1
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.html23
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss17
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts108
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.html2
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.ts16
-rw-r--r--client/src/app/videos/+video-watch/modal/video-share.component.html192
-rw-r--r--client/src/app/videos/+video-watch/modal/video-share.component.scss68
-rw-r--r--client/src/app/videos/+video-watch/modal/video-share.component.ts91
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.html2
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.scss1
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts108
-rw-r--r--client/src/app/videos/video-list/video-local.component.ts2
-rw-r--r--client/src/app/videos/video-list/video-overview.component.html6
-rw-r--r--client/src/app/videos/video-list/video-overview.component.scss60
-rw-r--r--client/src/app/videos/video-list/video-recently-added.component.ts3
-rw-r--r--client/src/app/videos/video-list/video-trending.component.ts2
-rw-r--r--client/src/app/videos/video-list/video-user-subscriptions.component.ts3
-rw-r--r--client/src/assets/player/peertube-player-local-storage.ts2
-rw-r--r--client/src/assets/player/peertube-player-manager.ts36
-rw-r--r--client/src/assets/player/utils.ts51
-rw-r--r--client/src/assets/player/videojs-components/peertube-link-button.ts2
-rw-r--r--client/src/sass/application.scss118
-rw-r--r--client/src/sass/bootstrap.scss138
-rw-r--r--client/src/sass/include/_miniature.scss97
-rw-r--r--client/src/sass/include/_mixins.scss25
-rw-r--r--client/src/sass/include/_variables.scss4
-rw-r--r--client/src/sass/player/_player-variables.scss8
-rw-r--r--client/src/sass/player/context-menu.scss4
-rw-r--r--client/src/sass/player/peertube-skin.scss12
-rw-r--r--client/src/sass/player/settings-menu.scss2
-rw-r--r--client/src/standalone/videos/embed-api.ts130
-rw-r--r--client/src/standalone/videos/embed.html2
-rw-r--r--client/src/standalone/videos/embed.ts205
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 @@
1import { by, element } from 'protractor'
2
3export 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 @@
1import { by, element } from 'protractor'
2
3export 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 @@
1import { browser, by, element } from 'protractor' 1import { browser, by, element, ElementFinder, ExpectedConditions } from 'protractor'
2 2
3export class VideoWatchPage { 3export 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'
2import { VideoUploadPage } from './po/video-upload.po' 2import { VideoUploadPage } from './po/video-upload.po'
3import { LoginPage } from './po/login.po' 3import { LoginPage } from './po/login.po'
4import { browser } from 'protractor' 4import { browser } from 'protractor'
5import { VideoUpdatePage } from './po/video-update.po'
6import { MyAccountPage } from './po/my-account'
7
8async 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
17async function isMobileDevice () {
18 const caps = await browser.getCapabilities()
19 return caps.get('realMobile') === 'true' || caps.get('realMobile') === true
20}
21
22async function isSafari () {
23 const caps = await browser.getCapabilities()
24 return caps.get('browserName') && caps.get('browserName').toLowerCase() === 'safari'
25}
5 26
6describe('Videos workflow', () => { 27describe('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
10a {
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 @@
1import { Component, OnInit } from '@angular/core'
2import { FollowService } from '@app/shared/instance/follow.service'
3import { ComponentPagination, hasMoreItems } from '@app/shared/rest/component-pagination.model'
4import { Notifier } from '@app/core'
5import { RestService } from '@app/shared'
6import { 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
14export 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'
4import { AboutComponent } from './about.component' 4import { AboutComponent } from './about.component'
5import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component' 5import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component'
6import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component' 6import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component'
7import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component'
7 8
8const aboutRoutes: Routes = [ 9const 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'
6import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component' 6import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component'
7import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component' 7import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component'
8import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component' 8import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
9import { 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
8a.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'
3import { Account } from '@app/shared/account/account.model' 3import { Account } from '@app/shared/account/account.model'
4import { AccountService } from '@app/shared/account/account.service' 4import { AccountService } from '@app/shared/account/account.service'
5import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' 5import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
6import { flatMap, map, tap } from 'rxjs/operators' 6import { concatMap, map, switchMap, tap } from 'rxjs/operators'
7import { Subscription } from 'rxjs' 7import { from, Subscription } from 'rxjs'
8import { VideoChannel } from '@app/shared/video-channel/video-channel.model' 8import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
9import { Video } from '@app/shared/video/video.model'
10import { AuthService } from '@app/core'
11import { VideoService } from '@app/shared/video/video.service'
12import { VideoSortField } from '@app/shared/video/sort-field.type'
13import { 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'
15export class AccountVideoChannelsComponent implements OnInit, OnDestroy { 20export 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
9const accountsRoutes: Routes = [ 9const 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'
5import { SharedModule } from '../shared' 5import { SharedModule } from '../shared'
6import { AdminRoutingModule } from './admin-routing.module' 6import { AdminRoutingModule } from './admin-routing.module'
7import { AdminComponent } from './admin.component' 7import { AdminComponent } from './admin.component'
8import { FollowersListComponent, FollowingAddComponent, FollowsComponent, FollowService } from './follows' 8import { FollowersListComponent, FollowingAddComponent, FollowsComponent } from './follows'
9import { FollowingListComponent } from './follows/following-list/following-list.component' 9import { FollowingListComponent } from './follows/following-list/following-list.component'
10import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users' 10import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users'
11import { 11import {
@@ -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
15export class EditCustomConfigComponent extends FormReactive implements OnInit { 15export 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'
3import { SortMeta } from 'primeng/primeng' 3import { SortMeta } from 'primeng/primeng'
4import { ActorFollow } from '../../../../../../shared/models/actors/follow.model' 4import { ActorFollow } from '../../../../../../shared/models/actors/follow.model'
5import { RestPagination, RestTable } from '../../../shared' 5import { RestPagination, RestTable } from '../../../shared'
6import { FollowService } from '../shared' 6import { FollowService } from '@app/shared/instance/follow.service'
7import { I18n } from '@ngx-translate/i18n-polyfill' 7import { 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'
3import { Notifier } from '@app/core' 3import { Notifier } from '@app/core'
4import { ConfirmService } from '../../../core' 4import { ConfirmService } from '../../../core'
5import { validateHost } from '../../../shared' 5import { validateHost } from '../../../shared'
6import { FollowService } from '../shared' 6import { FollowService } from '@app/shared/instance/follow.service'
7import { I18n } from '@ngx-translate/i18n-polyfill' 7import { 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'
4import { ActorFollow } from '../../../../../../shared/models/actors/follow.model' 4import { ActorFollow } from '../../../../../../shared/models/actors/follow.model'
5import { ConfirmService } from '../../../core/confirm/confirm.service' 5import { ConfirmService } from '../../../core/confirm/confirm.service'
6import { RestPagination, RestTable } from '../../../shared' 6import { RestPagination, RestTable } from '../../../shared'
7import { FollowService } from '../shared' 7import { FollowService } from '@app/shared/instance/follow.service'
8import { I18n } from '@ngx-translate/i18n-polyfill' 8import { 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 @@
1export * from './following-add' 1export * from './following-add'
2export * from './followers-list' 2export * from './followers-list'
3export * from './following-list' 3export * from './following-list'
4export * from './shared'
5export * from './follows.component' 4export * from './follows.component'
6export * from './follows.routes' 5export * 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 @@
1export * 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'
7export abstract class UserEdit extends FormReactive { 7export 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
4input[type=password],
5input[type=email] {
6 @include peertube-input-text(340px);
7
8 display: block;
9}
10
11input[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 @@
1import { Component, OnInit } from '@angular/core'
2import { AuthService, Notifier, ServerService } from '@app/core'
3import { FormReactive, UserService } from '../../../shared'
4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
6import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service'
7import { User } from '../../../../../../shared'
8import { 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})
15export 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
7import { VideoPlaylistValidatorsService } from '@app/shared' 7import { VideoPlaylistValidatorsService } from '@app/shared'
8import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model' 8import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model'
9import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' 9import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
10import { VideoConstant } from '@shared/models'
11import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model' 10import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model'
12import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils' 11import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils'
13 12
@@ -18,7 +17,6 @@ import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils'
18}) 17})
19export class MyAccountVideoPlaylistCreateComponent extends MyAccountVideoPlaylistEdit implements OnInit { 18export 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 @@
1import { FormReactive } from '@app/shared' 1import { FormReactive } from '@app/shared'
2import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
3import { ServerService } from '@app/core'
4import { VideoPlaylist } from '@shared/models/videos/playlist/video-playlist.model' 2import { VideoPlaylist } from '@shared/models/videos/playlist/video-playlist.model'
3import { VideoConstant, VideoPlaylistPrivacy } from '@shared/models'
5 4
6export abstract class MyAccountVideoPlaylistEdit extends FormReactive { 5export 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'
9import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' 9import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
10import { VideoPlaylistValidatorsService } from '@app/shared' 10import { VideoPlaylistValidatorsService } from '@app/shared'
11import { VideoPlaylistUpdate } from '@shared/models/videos/playlist/video-playlist-update.model' 11import { VideoPlaylistUpdate } from '@shared/models/videos/playlist/video-playlist-update.model'
12import { VideoConstant } from '@shared/models'
13import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model'
14import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' 12import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
13import { 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'
21export class MyAccountVideoPlaylistUpdateComponent extends MyAccountVideoPlaylistEdit implements OnInit, OnDestroy { 20export 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'
38import { DragDropModule } from '@angular/cdk/drag-drop' 38import { DragDropModule } from '@angular/cdk/drag-drop'
39import { 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
7header {
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 @@
1import { Component } from '@angular/core'
2import { 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})
10export 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 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core' 3import { MetaGuard } from '@ngx-meta/core'
4import { SignupComponent } from './signup.component' 4import { RegisterComponent } from './register.component'
5import { ServerConfigResolver } from '@app/core/routing/server-config-resolver.service' 5import { ServerConfigResolver } from '@app/core/routing/server-config-resolver.service'
6import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service'
6 7
7const signupRoutes: Routes = [ 8const 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})
27export class SignupRoutingModule {} 28export 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 @@
1import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
2import { AuthService } from '@app/core'
3import { FormReactive, UserService, VideoChannelValidatorsService } from '@app/shared'
4import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
5import { FormGroup } from '@angular/forms'
6import { pairwise } from 'rxjs/operators'
7import { 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})
14export 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 @@
1import { Component, EventEmitter, OnInit, Output } from '@angular/core'
2import { AuthService } from '@app/core'
3import { FormReactive, UserService, UserValidatorsService } from '@app/shared'
4import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
5import { FormGroup } from '@angular/forms'
6import { pairwise } from 'rxjs/operators'
7import { 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})
14export 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
24my-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
42input: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
54input[type=submit],
55button {
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 @@
1import { Component } from '@angular/core'
2import { AuthService, Notifier, RedirectService, ServerService } from '@app/core'
3import { UserService, UserValidatorsService } from '@app/shared'
4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { UserRegister } from '@shared/models/users/user-register.model'
6import { FormGroup } from '@angular/forms'
7
8@Component({
9 selector: 'my-register',
10 templateUrl: './register.component.html',
11 styleUrls: [ './register.component.scss' ]
12})
13export 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 @@
1import { NgModule } from '@angular/core'
2import { RegisterRoutingModule } from './register-routing.module'
3import { RegisterComponent } from './register.component'
4import { SharedModule } from '@app/shared'
5import { CdkStepperModule } from '@angular/cdk/stepper'
6import { RegisterStepChannelComponent } from './register-step-channel.component'
7import { RegisterStepUserComponent } from './register-step-user.component'
8import { CustomStepperComponent } from './custom-stepper.component'
9import { 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})
33export 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 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { Notifier } from '@app/core' 4import { AuthService, Notifier } from '@app/core'
5import { UserService } from '@app/shared' 5import { UserService } from '@app/shared'
6 6
7@Component({ 7@Component({
@@ -11,12 +11,15 @@ import { UserService } from '@app/shared'
11 11
12export class VerifyAccountEmailComponent implements OnInit { 12export 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 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3
4import { MetaGuard } from '@ngx-meta/core' 3import { MetaGuard } from '@ngx-meta/core'
5 4import { VerifyAccountEmailComponent } from './verify-account-email/verify-account-email.component'
6import { VerifyAccountEmailComponent } from '@app/+verify-account/verify-account-email/verify-account-email.component' 5import { VerifyAccountAskSendEmailComponent } from './verify-account-ask-send-email/verify-account-ask-send-email.component'
7import {
8 VerifyAccountAskSendEmailComponent
9} from '@app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component'
10 6
11const verifyAccountRoutes: Routes = [ 7const 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 @@
1import { NgModule } from '@angular/core'
2import { VerifyAccountRoutingModule } from './verify-account-routing.module'
3import { VerifyAccountEmailComponent } from './verify-account-email/verify-account-email.component'
4import { VerifyAccountAskSendEmailComponent } from './verify-account-ask-send-email/verify-account-ask-send-email.component'
5import { SharedModule } from '@app/shared'
6import { 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})
24export 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 @@
1import { NgModule } from '@angular/core'
2import { SignupSuccessComponent } from '../shared/signup-success.component'
3import { 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})
21export 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 @@
1svg {
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 @@
1import { 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})
8export 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 @@
1export * from '@app/+verify-account/verify-account-routing.module'
2export * 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 @@
1import { NgModule } from '@angular/core'
2
3import { VerifyAccountRoutingModule } from '@app/+verify-account/verify-account-routing.module'
4import { VerifyAccountEmailComponent } from '@app/+verify-account/verify-account-email/verify-account-email.component'
5import {
6 VerifyAccountAskSendEmailComponent
7} from '@app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component'
8import { 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})
27export 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'
5import { Subscription } from 'rxjs' 5import { Subscription } from 'rxjs'
6import { Notifier } from '@app/core' 6import { Notifier } from '@app/core'
7import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' 7import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
8import { ComponentPagination } from '@app/shared/rest/component-pagination.model' 8import { ComponentPagination, hasMoreItems } from '@app/shared/rest/component-pagination.model'
9import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' 9import { 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'
14import { LoginModule } from './login' 14import { LoginModule } from './login'
15import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu' 15import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu'
16import { SharedModule } from './shared' 16import { SharedModule } from './shared'
17import { SignupModule } from './signup'
18import { VideosModule } from './videos' 17import { VideosModule } from './videos'
19import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n' 18import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n'
20import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' 19import { 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'
20import { MessageService } from 'primeng/api' 20import { MessageService } from 'primeng/api'
21import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service' 21import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service'
22import { ServerConfigResolver } from './routing/server-config-resolver.service' 22import { ServerConfigResolver } from './routing/server-config-resolver.service'
23import { 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 @@
1import { Injectable } from '@angular/core'
2import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router'
3import { AuthService } from '../auth/auth.service'
4import { RedirectService } from './redirect.service'
5
6@Injectable()
7export 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
10import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' 10import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
11import { sortBy } from '@app/shared/misc/utils' 11import { sortBy } from '@app/shared/misc/utils'
12import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model' 12import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model'
13import { cloneDeep } from 'lodash-es'
13 14
14@Injectable() 15@Injectable()
15export class ServerService { 16export 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'
4import { SearchService } from '@app/search/search.service' 4import { SearchService } from '@app/search/search.service'
5import { SearchRoutingModule } from '@app/search/search-routing.module' 5import { SearchRoutingModule } from '@app/search/search-routing.module'
6import { SearchFiltersComponent } from '@app/search/search-filters.component' 6import { SearchFiltersComponent } from '@app/search/search-filters.component'
7import { 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
5export abstract class Actor implements ActorServer { 5export 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
10export class ButtonComponent { 10export 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
2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
3import { Notifier } from '@app/core' 3import { Notifier } from '@app/core'
4import { I18n } from '@ngx-translate/i18n-polyfill' 4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { 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 @@
1import { Component, forwardRef, Input } from '@angular/core' 1import { Component, forwardRef, Input, OnInit } from '@angular/core'
2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
3import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser' 3import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'
4import { ServerService } from '@app/core' 4import { 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})
18export class ImageUploadComponent implements ControlValueAccessor { 18export 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'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { SortMeta } from 'primeng/primeng' 4import { SortMeta } from 'primeng/primeng'
5import { Observable } from 'rxjs' 5import { Observable } from 'rxjs'
6import { ActorFollow, ResultList } from '../../../../../../shared' 6import { ActorFollow, ResultList } from '@shared/index'
7import { environment } from '../../../../environments/environment' 7import { environment } from '../../../environments/environment'
8import { RestExtractor, RestPagination, RestService } from '../../../shared' 8import { RestExtractor, RestPagination, RestService } from '../rest'
9 9
10@Injectable() 10@Injectable()
11export class FollowService { 11export 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'
53import { PeertubeCheckboxComponent } from '@app/shared/forms/peertube-checkbox.component' 53import { PeertubeCheckboxComponent } from '@app/shared/forms/peertube-checkbox.component'
54import { VideoImportService } from '@app/shared/video-import/video-import.service' 54import { VideoImportService } from '@app/shared/video-import/video-import.service'
55import { ActionDropdownComponent } from '@app/shared/buttons/action-dropdown.component' 55import { ActionDropdownComponent } from '@app/shared/buttons/action-dropdown.component'
56import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' 56import {
57 NgbCollapseModule,
58 NgbDropdownModule,
59 NgbModalModule,
60 NgbPopoverModule,
61 NgbTabsetModule,
62 NgbTooltipModule
63} from '@ng-bootstrap/ng-bootstrap'
57import { RemoteSubscribeComponent, SubscribeButtonComponent, UserSubscriptionService } from '@app/shared/user-subscription' 64import { RemoteSubscribeComponent, SubscribeButtonComponent, UserSubscriptionService } from '@app/shared/user-subscription'
58import { InstanceFeaturesTableComponent } from '@app/shared/instance/instance-features-table.component' 65import { InstanceFeaturesTableComponent } from '@app/shared/instance/instance-features-table.component'
59import { OverviewService } from '@app/shared/overview' 66import { OverviewService } from '@app/shared/overview'
@@ -69,7 +76,7 @@ import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/sha
69import { ConfirmComponent } from '@app/shared/confirm/confirm.component' 76import { ConfirmComponent } from '@app/shared/confirm/confirm.component'
70import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component' 77import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
71import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' 78import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
72import { ImageUploadComponent } from '@app/shared/images/image-upload.component' 79import { PreviewUploadComponent } from '@app/shared/images/preview-upload.component'
73import { GlobalIconComponent } from '@app/shared/images/global-icon.component' 80import { GlobalIconComponent } from '@app/shared/images/global-icon.component'
74import { VideoPlaylistMiniatureComponent } from '@app/shared/video-playlist/video-playlist-miniature.component' 81import { VideoPlaylistMiniatureComponent } from '@app/shared/video-playlist/video-playlist-miniature.component'
75import { VideoAddToPlaylistComponent } from '@app/shared/video-playlist/video-add-to-playlist.component' 82import { VideoAddToPlaylistComponent } from '@app/shared/video-playlist/video-add-to-playlist.component'
@@ -85,6 +92,7 @@ import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklis
85import { VideoDownloadComponent } from '@app/shared/video/modals/video-download.component' 92import { VideoDownloadComponent } from '@app/shared/video/modals/video-download.component'
86import { VideoReportComponent } from '@app/shared/video/modals/video-report.component' 93import { VideoReportComponent } from '@app/shared/video/modals/video-report.component'
87import { ClipboardModule } from 'ngx-clipboard' 94import { ClipboardModule } from 'ngx-clipboard'
95import { 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'
9import { SortMeta } from 'primeng/api' 9import { SortMeta } from 'primeng/api'
10import { BytesPipe } from 'ngx-pipes' 10import { BytesPipe } from 'ngx-pipes'
11import { I18n } from '@ngx-translate/i18n-polyfill' 11import { I18n } from '@ngx-translate/i18n-polyfill'
12import { UserRegister } from '@shared/models/users/user-register.model'
12 13
13@Injectable() 14@Injectable()
14export class UserService { 15export 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'
2import { Injectable } from '@angular/core' 2import { Injectable } from '@angular/core'
3import { Observable, ReplaySubject } from 'rxjs' 3import { Observable, ReplaySubject } from 'rxjs'
4import { RestExtractor } from '../rest/rest-extractor.service' 4import { RestExtractor } from '../rest/rest-extractor.service'
5import { HttpClient } from '@angular/common/http' 5import { HttpClient, HttpParams } from '@angular/common/http'
6import { VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '../../../../../shared/models/videos' 6import { VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '../../../../../shared/models/videos'
7import { AccountService } from '../account/account.service' 7import { AccountService } from '../account/account.service'
8import { ResultList } from '../../../../../shared' 8import { ResultList } from '../../../../../shared'
@@ -10,6 +10,8 @@ import { VideoChannel } from './video-channel.model'
10import { environment } from '../../../environments/environment' 10import { environment } from '../../../environments/environment'
11import { Account } from '@app/shared/account/account.model' 11import { Account } from '@app/shared/account/account.model'
12import { Avatar } from '../../../../../shared/models/avatars/avatar.model' 12import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
13import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
14import { RestService } from '@app/shared/rest'
13 15
14@Injectable() 16@Injectable()
15export class VideoChannelService { 17export 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
11import { Syndication } from '@app/shared/video/syndication.model' 11import { Syndication } from '@app/shared/video/syndication.model'
12import { Notifier, ServerService } from '@app/core' 12import { Notifier, ServerService } from '@app/core'
13import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' 13import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
14import { I18n } from '@ngx-translate/i18n-polyfill'
15import { isLastMonth, isLastWeek, isToday, isYesterday } from '@shared/core-utils/miscs/date'
16
17enum GroupDate {
18 UNKNOWN = 0,
19 TODAY = 1,
20 YESTERDAY = 2,
21 LAST_WEEK = 3,
22 LAST_MONTH = 4,
23 OLDER = 5
24}
14 25
15export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableForReuseHook { 26export 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 @@
1import { Component, ElementRef, ViewChild } from '@angular/core' 1import { Component, ElementRef, ViewChild } from '@angular/core'
2import { VideoDetails } from '../../../shared/video/video-details.model' 2import { VideoDetails } from '../../../shared/video/video-details.model'
3import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 3import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap'
4import { I18n } from '@ngx-translate/i18n-polyfill' 4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { Notifier } from '@app/core' 5import { Notifier } from '@app/core'
6 6
@@ -12,10 +12,11 @@ import { Notifier } from '@app/core'
12export class VideoDownloadComponent { 12export 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 @@
1import { UserRight, VideoConstant, VideoDetails as VideoDetailsServerModel, VideoFile, VideoState } from '../../../../../shared' 1import { VideoConstant, VideoDetails as VideoDetailsServerModel, VideoFile, VideoState } from '../../../../../shared'
2import { AuthUser } from '../../core'
3import { Video } from '../../shared/video/video.model' 2import { Video } from '../../shared/video/video.model'
4import { Account } from '@app/shared/account/account.model' 3import { Account } from '@app/shared/account/account.model'
5import { VideoChannel } from '@app/shared/video-channel/video-channel.model' 4import { 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'
20import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive' 20import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive'
21import { VideoSortField } from '@app/shared/video/sort-field.type' 21import { VideoSortField } from '@app/shared/video/sort-field.type'
22import { ComponentPagination } from '@app/shared/rest/component-pagination.model' 22import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
23import { I18n } from '@ngx-translate/i18n-polyfill'
23 24
24export type SelectionType = { [ id: number ]: boolean } 25export 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 @@
1export * from './signup-routing.module'
2export * from './signup.component'
3export * 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
4my-instance-features-table {
5 display: block;
6
7 margin-bottom: 40px;
8}
9
10form {
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
26input: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
36input[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 @@
1import { Component, OnInit } from '@angular/core'
2import { AuthService, Notifier, RedirectService, ServerService } from '@app/core'
3import { UserCreate } from '../../../../shared'
4import { FormReactive, UserService, UserValidatorsService } from '../shared'
5import { I18n } from '@ngx-translate/i18n-polyfill'
6import { 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})
13export 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 @@
1import { NgModule } from '@angular/core'
2
3import { SignupRoutingModule } from './signup-routing.module'
4import { SignupComponent } from './signup.component'
5import { 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})
24export 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
13import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' 13import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model'
14import { removeElementFromArray } from '@app/shared/misc/utils' 14import { removeElementFromArray } from '@app/shared/misc/utils'
15import { VideoConstant, VideoPrivacy } from '../../../../../../shared' 15import { VideoConstant, VideoPrivacy } from '../../../../../../shared'
16import { 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-
14export abstract class VideoSend extends FormReactive implements OnInit { 14export 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'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { LoadingBarService } from '@ngx-loading-bar/core' 4import { LoadingBarService } from '@ngx-loading-bar/core'
5import { Notifier } from '@app/core' 5import { Notifier } from '@app/core'
6import { VideoConstant, VideoPrivacy } from '../../../../../shared/models/videos'
7import { ServerService } from '../../core' 6import { ServerService } from '../../core'
8import { FormReactive } from '../../shared' 7import { FormReactive } from '../../shared'
9import { VideoEdit } from '../../shared/video/video-edit.model' 8import { VideoEdit } from '../../shared/video/video-edit.model'
@@ -13,6 +12,7 @@ import { FormValidatorService } from '@app/shared/forms/form-validators/form-val
13import { VideoCaptionService } from '@app/shared/video-caption' 12import { VideoCaptionService } from '@app/shared/video-caption'
14import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' 13import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model'
15import { VideoDetails } from '@app/shared/video/video-details.model' 14import { VideoDetails } from '@app/shared/video/video-details.model'
15import { 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
28input.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'
3import { VideoDetails } from '../../../shared/video/video-details.model' 3import { VideoDetails } from '../../../shared/video/video-details.model'
4import { buildVideoEmbed, buildVideoLink } from '../../../../assets/player/utils' 4import { buildVideoEmbed, buildVideoLink } from '../../../../assets/player/utils'
5import { I18n } from '@ngx-translate/i18n-polyfill' 5import { I18n } from '@ngx-translate/i18n-polyfill'
6import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 6import { NgbModal, NgbTabChangeEvent } from '@ng-bootstrap/ng-bootstrap'
7import { durationToString } from '@app/shared/misc/utils' 7import { VideoCaption } from '@shared/models'
8
9type 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'
6import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component' 6import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component'
7import { MetaService } from '@ngx-meta/core' 7import { MetaService } from '@ngx-meta/core'
8import { Notifier, ServerService } from '@app/core' 8import { Notifier, ServerService } from '@app/core'
9import { forkJoin, Subscription } from 'rxjs' 9import { forkJoin, Observable, Subscription } from 'rxjs'
10import { Hotkey, HotkeysService } from 'angular2-hotkeys' 10import { Hotkey, HotkeysService } from 'angular2-hotkeys'
11import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared' 11import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared'
12import { AuthService, ConfirmService } from '../../core' 12import { AuthService, ConfirmService } from '../../core'
@@ -20,6 +20,7 @@ import { environment } from '../../../environments/environment'
20import { VideoCaptionService } from '@app/shared/video-caption' 20import { VideoCaptionService } from '@app/shared/video-caption'
21import { MarkdownService } from '@app/shared/renderer' 21import { MarkdownService } from '@app/shared/renderer'
22import { 22import {
23 CustomizationOptions,
23 P2PMediaLoaderOptions, 24 P2PMediaLoaderOptions,
24 PeertubePlayerManager, 25 PeertubePlayerManager,
25 PeertubePlayerManagerOptions, 26 PeertubePlayerManagerOptions,
@@ -28,8 +29,9 @@ import {
28import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' 29import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
29import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' 30import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
30import { Video } from '@app/shared/video/video.model' 31import { Video } from '@app/shared/video/video.model'
31import { isWebRTCDisabled } from '../../../assets/player/utils' 32import { isWebRTCDisabled, timeToInt } from '../../../assets/player/utils'
32import { VideoWatchPlaylistComponent } from '@app/videos/+video-watch/video-watch-playlist.component' 33import { VideoWatchPlaylistComponent } from '@app/videos/+video-watch/video-watch-playlist.component'
34import { 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'
17export class VideoRecentlyAddedComponent extends AbstractVideoList implements OnInit, OnDestroy { 17export 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
35function saveVolumeInStore (value: number) { 35function 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
42export type CommonOptions = { 42export 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
54export 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
30function buildVideoLink (time?: number, url?: string) { 30function 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
286ngb-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
108ngb-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
15body {
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 @@
1import './embed.scss'
2
3import * as Channel from 'jschannel'
4import { PeerTubeResolution } from '../player/definitions'
5import { 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 */
11export 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 @@
1import './embed.scss' 1import './embed.scss'
2 2
3import * as Channel from 'jschannel'
4
5import { peertubeTranslate, ResultList, ServerConfig, VideoDetails } from '../../../../shared' 3import { peertubeTranslate, ResultList, ServerConfig, VideoDetails } from '../../../../shared'
6import { PeerTubeResolution } from '../player/definitions'
7import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings' 4import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings'
8import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' 5import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model'
9import { 6import {
@@ -13,133 +10,9 @@ import {
13 PlayerMode 10 PlayerMode
14} from '../../assets/player/peertube-player-manager' 11} from '../../assets/player/peertube-player-manager'
15import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type' 12import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type'
13import { PeerTubeEmbedApi } from './embed-api'
16 14
17/** 15export class PeerTubeEmbed {
18 * Embed API exposes control of the embed player to the outside world via
19 * JSChannels and window.postMessage
20 */
21class 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
142class 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