aboutsummaryrefslogtreecommitdiffhomepage
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/.bootstraprc10
-rw-r--r--client/config/webpack.common.js18
-rw-r--r--client/config/webpack.video-embed.js3
-rw-r--r--client/package.json6
-rw-r--r--client/src/app/+admin/admin.component.html27
-rw-r--r--client/src/app/+admin/admin.component.scss0
-rw-r--r--client/src/app/+admin/admin.component.ts26
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.html26
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.scss3
-rw-r--r--client/src/app/+admin/follows/following-add/following-add.component.html47
-rw-r--r--client/src/app/+admin/follows/following-add/following-add.component.scss10
-rw-r--r--client/src/app/+admin/follows/following-add/following-add.component.ts94
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.html34
-rw-r--r--client/src/app/+admin/follows/follows.component.html6
-rw-r--r--client/src/app/+admin/follows/follows.component.scss23
-rw-r--r--client/src/app/+admin/follows/follows.component.ts2
-rw-r--r--client/src/app/+admin/jobs/jobs-list/jobs-list.component.html36
-rw-r--r--client/src/app/+admin/jobs/jobs-list/jobs-list.component.scss3
-rw-r--r--client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts24
-rw-r--r--client/src/app/+admin/jobs/shared/job.service.ts7
-rw-r--r--client/src/app/+admin/users/shared/user.service.ts14
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.html123
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.scss18
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.html57
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.scss14
-rw-r--r--client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html41
-rw-r--r--client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.scss6
-rw-r--r--client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.ts3
-rw-r--r--client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.html2
-rw-r--r--client/src/app/account/account-change-password/account-change-password.component.html24
-rw-r--r--client/src/app/account/account-details/account-details.component.html16
-rw-r--r--client/src/app/account/account-routing.module.ts27
-rw-r--r--client/src/app/account/account-settings/account-change-password/account-change-password.component.html20
-rw-r--r--client/src/app/account/account-settings/account-change-password/account-change-password.component.scss16
-rw-r--r--client/src/app/account/account-settings/account-change-password/account-change-password.component.ts (renamed from client/src/app/account/account-change-password/account-change-password.component.ts)9
-rw-r--r--client/src/app/account/account-settings/account-change-password/index.ts (renamed from client/src/app/account/account-change-password/index.ts)0
-rw-r--r--client/src/app/account/account-settings/account-details/account-details.component.html14
-rw-r--r--client/src/app/account/account-settings/account-details/account-details.component.scss13
-rw-r--r--client/src/app/account/account-settings/account-details/account-details.component.ts (renamed from client/src/app/account/account-details/account-details.component.ts)19
-rw-r--r--client/src/app/account/account-settings/account-details/index.ts (renamed from client/src/app/account/account-details/index.ts)0
-rw-r--r--client/src/app/account/account-settings/account-settings.component.html15
-rw-r--r--client/src/app/account/account-settings/account-settings.component.scss28
-rw-r--r--client/src/app/account/account-settings/account-settings.component.ts22
-rw-r--r--client/src/app/account/account-videos/account-videos.component.html39
-rw-r--r--client/src/app/account/account-videos/account-videos.component.scss96
-rw-r--r--client/src/app/account/account-videos/account-videos.component.ts97
-rw-r--r--client/src/app/account/account.component.html26
-rw-r--r--client/src/app/account/account.component.scss3
-rw-r--r--client/src/app/account/account.component.ts24
-rw-r--r--client/src/app/account/account.module.ts13
-rw-r--r--client/src/app/app-routing.module.ts2
-rw-r--r--client/src/app/app.component.html41
-rw-r--r--client/src/app/app.component.scss140
-rw-r--r--client/src/app/app.component.ts17
-rw-r--r--client/src/app/app.module.ts7
-rw-r--r--client/src/app/core/auth/auth.service.ts61
-rw-r--r--client/src/app/core/confirm/confirm.component.html6
-rw-r--r--client/src/app/core/confirm/confirm.component.ts3
-rw-r--r--client/src/app/core/core.module.ts8
-rw-r--r--client/src/app/core/index.ts1
-rw-r--r--client/src/app/core/menu/index.ts2
-rw-r--r--client/src/app/core/menu/menu-admin.component.html35
-rw-r--r--client/src/app/core/menu/menu-admin.component.ts33
-rw-r--r--client/src/app/core/menu/menu.component.html55
-rw-r--r--client/src/app/core/menu/menu.component.scss51
-rw-r--r--client/src/app/core/server/server.service.ts38
-rw-r--r--client/src/app/header/header.component.html10
-rw-r--r--client/src/app/header/header.component.scss58
-rw-r--r--client/src/app/header/header.component.ts28
-rw-r--r--client/src/app/header/index.ts1
-rw-r--r--client/src/app/login/login.component.html53
-rw-r--r--client/src/app/login/login.component.scss9
-rw-r--r--client/src/app/login/login.component.ts3
-rw-r--r--client/src/app/menu/index.ts1
-rw-r--r--client/src/app/menu/menu.component.html50
-rw-r--r--client/src/app/menu/menu.component.scss193
-rw-r--r--client/src/app/menu/menu.component.ts (renamed from client/src/app/core/menu/menu.component.ts)19
-rw-r--r--client/src/app/shared/account/account.model.ts20
-rw-r--r--client/src/app/shared/forms/form-validators/host.validator.ts10
-rw-r--r--client/src/app/shared/forms/form-validators/video-abuse.ts6
-rw-r--r--client/src/app/shared/forms/form-validators/video.ts28
-rw-r--r--client/src/app/shared/index.ts1
-rw-r--r--client/src/app/shared/misc/button.component.scss27
-rw-r--r--client/src/app/shared/misc/delete-button.component.html4
-rw-r--r--client/src/app/shared/misc/delete-button.component.ts10
-rw-r--r--client/src/app/shared/misc/edit-button.component.html4
-rw-r--r--client/src/app/shared/misc/edit-button.component.ts11
-rw-r--r--client/src/app/shared/misc/from-now.pipe.ts36
-rw-r--r--client/src/app/shared/misc/number-formatter.pipe.ts19
-rw-r--r--client/src/app/shared/misc/utils.ts23
-rw-r--r--client/src/app/shared/search/index.ts4
-rw-r--r--client/src/app/shared/search/search-field.type.ts1
-rw-r--r--client/src/app/shared/search/search.component.html22
-rw-r--r--client/src/app/shared/search/search.component.scss51
-rw-r--r--client/src/app/shared/search/search.component.ts69
-rw-r--r--client/src/app/shared/search/search.model.ts6
-rw-r--r--client/src/app/shared/search/search.service.ts18
-rw-r--r--client/src/app/shared/shared.module.ts56
-rw-r--r--client/src/app/shared/users/user.model.ts23
-rw-r--r--client/src/app/shared/video/abstract-video-list.html20
-rw-r--r--client/src/app/shared/video/abstract-video-list.scss7
-rw-r--r--client/src/app/shared/video/abstract-video-list.ts133
-rw-r--r--client/src/app/shared/video/sort-field.type.ts (renamed from client/src/app/videos/shared/sort-field.type.ts)0
-rw-r--r--client/src/app/shared/video/video-details.model.ts (renamed from client/src/app/videos/shared/video-details.model.ts)16
-rw-r--r--client/src/app/shared/video/video-edit.model.ts (renamed from client/src/app/videos/shared/video-edit.model.ts)26
-rw-r--r--client/src/app/shared/video/video-miniature.component.html17
-rw-r--r--client/src/app/shared/video/video-miniature.component.scss44
-rw-r--r--client/src/app/shared/video/video-miniature.component.ts (renamed from client/src/app/videos/video-list/shared/video-miniature.component.ts)6
-rw-r--r--client/src/app/shared/video/video-pagination.model.ts (renamed from client/src/app/videos/shared/video-pagination.model.ts)2
-rw-r--r--client/src/app/shared/video/video-thumbnail.component.html10
-rw-r--r--client/src/app/shared/video/video-thumbnail.component.scss28
-rw-r--r--client/src/app/shared/video/video-thumbnail.component.ts12
-rw-r--r--client/src/app/shared/video/video.model.ts (renamed from client/src/app/videos/shared/video.model.ts)10
-rw-r--r--client/src/app/shared/video/video.service.ts (renamed from client/src/app/videos/shared/video.service.ts)50
-rw-r--r--client/src/app/signup/signup.component.html22
-rw-r--r--client/src/app/signup/signup.component.scss9
-rw-r--r--client/src/app/signup/signup.component.ts3
-rw-r--r--client/src/app/videos/+video-edit/shared/video-description.component.html (renamed from client/src/app/videos/shared/video-description.component.html)4
-rw-r--r--client/src/app/videos/+video-edit/shared/video-description.component.scss24
-rw-r--r--client/src/app/videos/+video-edit/shared/video-description.component.ts (renamed from client/src/app/videos/shared/video-description.component.ts)10
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.html86
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.scss148
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.ts83
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.module.ts11
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.html152
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.scss96
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.ts198
-rw-r--r--client/src/app/videos/+video-edit/video-add.module.ts4
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.html105
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.ts66
-rw-r--r--client/src/app/videos/+video-watch/video-download.component.html11
-rw-r--r--client/src/app/videos/+video-watch/video-download.component.scss23
-rw-r--r--client/src/app/videos/+video-watch/video-download.component.ts6
-rw-r--r--client/src/app/videos/+video-watch/video-report.component.html12
-rw-r--r--client/src/app/videos/+video-watch/video-report.component.ts8
-rw-r--r--client/src/app/videos/+video-watch/video-share.component.html2
-rw-r--r--client/src/app/videos/+video-watch/video-share.component.ts4
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.html288
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.scss381
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts67
-rw-r--r--client/src/app/videos/+video-watch/video-watch.module.ts5
-rw-r--r--client/src/app/videos/shared/index.ts7
-rw-r--r--client/src/app/videos/shared/video-description.component.scss15
-rw-r--r--client/src/app/videos/video-list/index.ts6
-rw-r--r--client/src/app/videos/video-list/my-videos.component.ts36
-rw-r--r--client/src/app/videos/video-list/shared/abstract-video-list.html28
-rw-r--r--client/src/app/videos/video-list/shared/abstract-video-list.scss37
-rw-r--r--client/src/app/videos/video-list/shared/abstract-video-list.ts104
-rw-r--r--client/src/app/videos/video-list/shared/index.ts3
-rw-r--r--client/src/app/videos/video-list/shared/video-miniature.component.html33
-rw-r--r--client/src/app/videos/video-list/shared/video-miniature.component.scss101
-rw-r--r--client/src/app/videos/video-list/shared/video-sort.component.html5
-rw-r--r--client/src/app/videos/video-list/shared/video-sort.component.ts39
-rw-r--r--client/src/app/videos/video-list/video-list.component.ts94
-rw-r--r--client/src/app/videos/video-list/video-recently-added.component.ts32
-rw-r--r--client/src/app/videos/video-list/video-search.component.ts51
-rw-r--r--client/src/app/videos/video-list/video-trending.component.ts32
-rw-r--r--client/src/app/videos/videos-routing.module.ts33
-rw-r--r--client/src/app/videos/videos.module.ts16
-rw-r--r--client/src/assets/favicon.pngbin2335 -> 0 bytes
-rw-r--r--client/src/assets/images/admin/add.svg13
-rw-r--r--client/src/assets/images/default-avatar.pngbin0 -> 1674 bytes
-rw-r--r--client/src/assets/images/favicon.pngbin0 -> 833 bytes
-rw-r--r--client/src/assets/images/global/delete-grey.svg14
-rw-r--r--client/src/assets/images/global/delete-white.svg14
-rw-r--r--client/src/assets/images/global/edit.svg15
-rw-r--r--client/src/assets/images/global/validate.svg14
-rw-r--r--client/src/assets/images/header/menu.svg14
-rw-r--r--client/src/assets/images/header/search.svg12
-rw-r--r--client/src/assets/images/header/upload.svg16
-rw-r--r--client/src/assets/images/logo.svg118
-rw-r--r--client/src/assets/images/menu/administration.svg14
-rw-r--r--client/src/assets/images/menu/recently-added.svg13
-rw-r--r--client/src/assets/images/menu/trending.svg16
-rw-r--r--client/src/assets/images/video/alert.svg16
-rw-r--r--client/src/assets/images/video/dislike-grey.svg14
-rw-r--r--client/src/assets/images/video/dislike-white.svg14
-rw-r--r--client/src/assets/images/video/download-grey.svg16
-rw-r--r--client/src/assets/images/video/download-white.svg16
-rw-r--r--client/src/assets/images/video/eye-closed.svg18
-rw-r--r--client/src/assets/images/video/like-grey.svg15
-rw-r--r--client/src/assets/images/video/like-white.svg15
-rw-r--r--client/src/assets/images/video/more.svg11
-rw-r--r--client/src/assets/images/video/share.svg16
-rw-r--r--client/src/assets/images/video/upload.svg16
-rw-r--r--client/src/assets/logo.pngbin838 -> 0 bytes
-rw-r--r--client/src/assets/player/images/arrow-down.svg14
-rw-r--r--client/src/assets/player/images/arrow-up.svg14
-rw-r--r--client/src/assets/player/images/fullscreen.svg18
-rw-r--r--client/src/assets/player/images/volume-mute.svg16
-rw-r--r--client/src/assets/player/images/volume.svg13
-rw-r--r--client/src/assets/player/peertube-videojs-plugin.ts100
-rw-r--r--client/src/index.html2
-rw-r--r--client/src/sass/_mixins.scss95
-rw-r--r--client/src/sass/_variables.scss32
-rw-r--r--client/src/sass/application.scss298
-rw-r--r--client/src/sass/pre-customizations.scss1
-rw-r--r--client/src/sass/video-js-custom.scss573
-rw-r--r--client/src/standalone/videos/embed.html2
-rw-r--r--client/src/standalone/videos/embed.scss32
-rw-r--r--client/yarn.lock22
201 files changed, 4205 insertions, 2926 deletions
diff --git a/client/.bootstraprc b/client/.bootstraprc
index 6ceef4fe9..cc6768d43 100644
--- a/client/.bootstraprc
+++ b/client/.bootstraprc
@@ -84,19 +84,19 @@ styles:
84 navs: true 84 navs: true
85 navbar: false 85 navbar: false
86 breadcrumbs: false 86 breadcrumbs: false
87 pagination: true 87 pagination: false
88 pager: false 88 pager: false
89 labels: true 89 labels: false
90 badges: false 90 badges: false
91 jumbotron: false 91 jumbotron: false
92 thumbnails: true 92 thumbnails: false
93 alerts: true 93 alerts: true
94 progress-bars: true 94 progress-bars: false
95 media: true 95 media: true
96 list-group: false 96 list-group: false
97 panels: true 97 panels: true
98 wells: false 98 wells: false
99 responsive-embed: true 99 responsive-embed: false
100 close: true 100 close: true
101 101
102 # Components w/ JavaScript 102 # Components w/ JavaScript
diff --git a/client/config/webpack.common.js b/client/config/webpack.common.js
index 9cd33d2ed..f387b44f9 100644
--- a/client/config/webpack.common.js
+++ b/client/config/webpack.common.js
@@ -13,6 +13,7 @@ const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin')
13const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin') 13const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin')
14const InlineManifestWebpackPlugin = require('inline-manifest-webpack-plugin') 14const InlineManifestWebpackPlugin = require('inline-manifest-webpack-plugin')
15const ngcWebpack = require('ngc-webpack') 15const ngcWebpack = require('ngc-webpack')
16const CopyWebpackPlugin = require('copy-webpack-plugin')
16 17
17const WebpackNotifierPlugin = require('webpack-notifier') 18const WebpackNotifierPlugin = require('webpack-notifier')
18 19
@@ -146,14 +147,15 @@ module.exports = function (options) {
146 loader: 'sass-resources-loader', 147 loader: 'sass-resources-loader',
147 options: { 148 options: {
148 resources: [ 149 resources: [
149 helpers.root('src/sass/_variables.scss') 150 helpers.root('src/sass/_variables.scss'),
151 helpers.root('src/sass/_mixins.scss')
150 ] 152 ]
151 } 153 }
152 } 154 }
153 ] 155 ]
154 }, 156 },
155 { test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, use: 'url-loader?limit=10000&minetype=application/font-woff' }, 157 { test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, use: 'url-loader?limit=10000&minetype=application/font-woff' },
156 { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, use: 'file-loader' }, 158 { test: /\.(otf|ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, use: 'url-loader?limit=10000' },
157 159
158 /* Raw loader support for *.html 160 /* Raw loader support for *.html
159 * Returns file content as string 161 * Returns file content as string
@@ -266,6 +268,17 @@ module.exports = function (options) {
266 inject: 'body' 268 inject: 'body'
267 }), 269 }),
268 270
271 new CopyWebpackPlugin([
272 {
273 from: helpers.root('src/assets/images/favicon.png'),
274 to: 'assets/images/favicon.png'
275 },
276 {
277 from: helpers.root('src/assets/images/default-avatar.png'),
278 to: 'assets/images/default-avatar.png'
279 }
280 ]),
281
269 /* 282 /*
270 * Plugin: ScriptExtHtmlWebpackPlugin 283 * Plugin: ScriptExtHtmlWebpackPlugin
271 * Description: Enhances html-webpack-plugin functionality 284 * Description: Enhances html-webpack-plugin functionality
@@ -289,6 +302,7 @@ module.exports = function (options) {
289 */ 302 */
290 new LoaderOptionsPlugin({ 303 new LoaderOptionsPlugin({
291 options: { 304 options: {
305 context: '',
292 sassLoader: { 306 sassLoader: {
293 precision: 10, 307 precision: 10,
294 includePaths: [ helpers.root('src/sass') ] 308 includePaths: [ helpers.root('src/sass') ]
diff --git a/client/config/webpack.video-embed.js b/client/config/webpack.video-embed.js
index fe40194cf..2b70b6681 100644
--- a/client/config/webpack.video-embed.js
+++ b/client/config/webpack.video-embed.js
@@ -74,7 +74,8 @@ module.exports = function (options) {
74 loader: 'sass-resources-loader', 74 loader: 'sass-resources-loader',
75 options: { 75 options: {
76 resources: [ 76 resources: [
77 helpers.root('src/sass/_variables.scss') 77 helpers.root('src/sass/_variables.scss'),
78 helpers.root('src/sass/_mixins.scss')
78 ] 79 ]
79 } 80 }
80 } 81 }
diff --git a/client/package.json b/client/package.json
index 39b3185cc..45f555f29 100644
--- a/client/package.json
+++ b/client/package.json
@@ -43,7 +43,6 @@
43 "@types/webpack": "^3.0.0", 43 "@types/webpack": "^3.0.0",
44 "@types/webtorrent": "^0.98.4", 44 "@types/webtorrent": "^0.98.4",
45 "add-asset-html-webpack-plugin": "^2.0.1", 45 "add-asset-html-webpack-plugin": "^2.0.1",
46 "angular-pipes": "^6.0.0",
47 "angular2-notifications": "^0.7.7", 46 "angular2-notifications": "^0.7.7",
48 "angular2-template-loader": "^0.6.0", 47 "angular2-template-loader": "^0.6.0",
49 "assets-webpack-plugin": "^3.4.0", 48 "assets-webpack-plugin": "^3.4.0",
@@ -70,8 +69,10 @@
70 "markdown-it": "^8.4.0", 69 "markdown-it": "^8.4.0",
71 "ng-router-loader": "^2.0.0", 70 "ng-router-loader": "^2.0.0",
72 "ngc-webpack": "3.2.2", 71 "ngc-webpack": "3.2.2",
73 "ngx-bootstrap": "1.9.3", 72 "ngx-bootstrap": "2.0.0-beta.9",
74 "ngx-chips": "1.5.3", 73 "ngx-chips": "1.5.3",
74 "ngx-infinite-scroll": "^0.7.0",
75 "ngx-pipes": "^2.0.5",
75 "node-sass": "^4.1.1", 76 "node-sass": "^4.1.1",
76 "normalize.css": "^7.0.0", 77 "normalize.css": "^7.0.0",
77 "optimize-js-plugin": "0.0.4", 78 "optimize-js-plugin": "0.0.4",
@@ -86,6 +87,7 @@
86 "sass-resources-loader": "^1.2.1", 87 "sass-resources-loader": "^1.2.1",
87 "script-ext-html-webpack-plugin": "^1.3.2", 88 "script-ext-html-webpack-plugin": "^1.3.2",
88 "source-map-loader": "^0.2.1", 89 "source-map-loader": "^0.2.1",
90 "source-sans-pro": "^2.0.10",
89 "standard": "^10.0.0", 91 "standard": "^10.0.0",
90 "string-replace-loader": "^1.0.3", 92 "string-replace-loader": "^1.0.3",
91 "style-loader": "^0.19.0", 93 "style-loader": "^0.19.0",
diff --git a/client/src/app/+admin/admin.component.html b/client/src/app/+admin/admin.component.html
new file mode 100644
index 000000000..0bf4c8aac
--- /dev/null
+++ b/client/src/app/+admin/admin.component.html
@@ -0,0 +1,27 @@
1<div class="row">
2 <div class="sub-menu">
3 <a *ngIf="hasUsersRight()" routerLink="/admin/users" routerLinkActive="active" class="title-page">
4 Users
5 </a>
6
7 <a *ngIf="hasServerFollowRight()" routerLink="/admin/follows" routerLinkActive="active" class="title-page">
8 Manage follows
9 </a>
10
11 <a *ngIf="hasVideoAbusesRight()" routerLink="/admin/video-abuses" routerLinkActive="active" class="title-page">
12 Video abuses
13 </a>
14
15 <a *ngIf="hasVideoBlacklistRight()" routerLink="/admin/video-blacklist" routerLinkActive="active" class="title-page">
16 Video blacklist
17 </a>
18
19 <a *ngIf="hasJobsRight()" routerLink="/admin/jobs" routerLinkActive="active" class="title-page">
20 Jobs
21 </a>
22 </div>
23
24 <div class="margin-content">
25 <router-outlet></router-outlet>
26 </div>
27</div>
diff --git a/client/src/app/+admin/admin.component.scss b/client/src/app/+admin/admin.component.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/client/src/app/+admin/admin.component.scss
diff --git a/client/src/app/+admin/admin.component.ts b/client/src/app/+admin/admin.component.ts
index ecd62ee61..75cd50cc7 100644
--- a/client/src/app/+admin/admin.component.ts
+++ b/client/src/app/+admin/admin.component.ts
@@ -1,7 +1,31 @@
1import { Component } from '@angular/core' 1import { Component } from '@angular/core'
2import { UserRight } from '../../../../shared'
3import { AuthService } from '../core/auth/auth.service'
2 4
3@Component({ 5@Component({
4 template: '<router-outlet></router-outlet>' 6 templateUrl: './admin.component.html',
7 styleUrls: [ './admin.component.scss' ]
5}) 8})
6export class AdminComponent { 9export class AdminComponent {
10 constructor (private auth: AuthService) {}
11
12 hasUsersRight () {
13 return this.auth.getUser().hasRight(UserRight.MANAGE_USERS)
14 }
15
16 hasServerFollowRight () {
17 return this.auth.getUser().hasRight(UserRight.MANAGE_SERVER_FOLLOW)
18 }
19
20 hasVideoAbusesRight () {
21 return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_ABUSES)
22 }
23
24 hasVideoBlacklistRight () {
25 return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
26 }
27
28 hasJobsRight () {
29 return this.auth.getUser().hasRight(UserRight.MANAGE_JOBS)
30 }
7} 31}
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.html b/client/src/app/+admin/follows/followers-list/followers-list.component.html
index 473801822..a24039fc6 100644
--- a/client/src/app/+admin/follows/followers-list/followers-list.component.html
+++ b/client/src/app/+admin/follows/followers-list/followers-list.component.html
@@ -1,16 +1,10 @@
1<div class="row"> 1<p-dataTable
2 <div class="content-padding"> 2 [value]="followers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
3 <h3>Followers list</h3> 3 sortField="createdAt" (onLazyLoad)="loadLazy($event)"
4 4>
5 <p-dataTable 5 <p-column field="id" header="ID"></p-column>
6 [value]="followers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" 6 <p-column field="follower.host" header="Host"></p-column>
7 sortField="createdAt" (onLazyLoad)="loadLazy($event)" 7 <p-column field="follower.score" header="Score"></p-column>
8 > 8 <p-column field="state" header="State"></p-column>
9 <p-column field="id" header="ID"></p-column> 9 <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
10 <p-column field="follower.host" header="Host"></p-column> 10</p-dataTable>
11 <p-column field="follower.score" header="Score"></p-column>
12 <p-column field="state" header="State"></p-column>
13 <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
14 </p-dataTable>
15 </div>
16</div>
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.scss b/client/src/app/+admin/follows/followers-list/followers-list.component.scss
index 0a0f621c6..e69de29bb 100644
--- a/client/src/app/+admin/follows/followers-list/followers-list.component.scss
+++ b/client/src/app/+admin/follows/followers-list/followers-list.component.scss
@@ -1,3 +0,0 @@
1.btn {
2 margin-top: 10px;
3}
diff --git a/client/src/app/+admin/follows/following-add/following-add.component.html b/client/src/app/+admin/follows/following-add/following-add.component.html
index 8e7dddc11..25bab9d0d 100644
--- a/client/src/app/+admin/follows/following-add/following-add.component.html
+++ b/client/src/app/+admin/follows/following-add/following-add.component.html
@@ -1,35 +1,22 @@
1<div class="row"> 1<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
2 <div class="content-padding">
3 2
4 <h3>Add following</h3> 3<form (ngSubmit)="addFollowing()">
4 <div class="form-group">
5 <label for="hosts">1 host (without "http://") per line</label>
5 6
6 <div *ngIf="error" class="alert alert-danger">{{ error }}</div> 7 <textarea
8 type="text" class="form-control" placeholder="example.com" id="hosts" name="hosts"
9 [(ngModel)]="hostsString" (ngModelChange)="onHostsChanged()" [ngClass]="{ 'input-error': hostsError }"
10 ></textarea>
7 11
8 <form (ngSubmit)="addFollowing()" [formGroup]="form"> 12 <div *ngIf="hostsError" class="form-error">
9 <div class="form-group" *ngFor="let host of hosts; let id = index; trackBy:customTrackBy"> 13 {{ hostsError }}
10 <label [for]="'host-' + id">Host (so without "http://")</label> 14 </div>
11 15 </div>
12 <div class="input-group">
13 <input
14 type="text" class="form-control" placeholder="example.com"
15 [id]="'host-' + id" [formControlName]="'host-' + id"
16 />
17 <span class="input-group-btn">
18 <button *ngIf="displayAddField(id)" (click)="addField()" class="btn btn-default" type="button">+</button>
19 <button *ngIf="displayRemoveField(id)" (click)="removeField(id)" class="btn btn-default" type="button">-</button>
20 </span>
21 </div>
22
23 <div [hidden]="form.controls['host-' + id].valid || form.controls['host-' + id].pristine" class="alert alert-warning">
24 It should be a valid host.
25 </div>
26 </div>
27
28 <div *ngIf="canMakeFriends() === false" class="alert alert-warning">
29 It seems that you are not on a HTTPS server. Your webserver need to have TLS activated in order to follow servers.
30 </div>
31 16
32 <input type="submit" value="Add following" class="btn btn-default" [disabled]="!isFormValid()"> 17 <div *ngIf="httpEnabled() === false" class="alert alert-warning">
33 </form> 18 It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
34 </div> 19 </div>
35</div> 20
21 <input type="submit" value="Add following" [disabled]="hostsError || !hostsString" class="btn btn-default">
22</form>
diff --git a/client/src/app/+admin/follows/following-add/following-add.component.scss b/client/src/app/+admin/follows/following-add/following-add.component.scss
index 5fde51636..2cb3efe28 100644
--- a/client/src/app/+admin/follows/following-add/following-add.component.scss
+++ b/client/src/app/+admin/follows/following-add/following-add.component.scss
@@ -1,7 +1,9 @@
1table { 1textarea {
2 margin-bottom: 40px; 2 height: 250px;
3} 3}
4 4
5.input-group-btn button { 5input[type=submit] {
6 width: 35px; 6 @include peertube-button;
7 @include orange-button;
7} 8}
9
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 814c6f1a1..bf842129d 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
@@ -1,9 +1,6 @@
1import { Component, OnInit } from '@angular/core' 1import { Component } from '@angular/core'
2import { FormControl, FormGroup } from '@angular/forms'
3import { Router } from '@angular/router' 2import { Router } from '@angular/router'
4
5import { NotificationsService } from 'angular2-notifications' 3import { NotificationsService } from 'angular2-notifications'
6
7import { ConfirmService } from '../../../core' 4import { ConfirmService } from '../../../core'
8import { validateHost } from '../../../shared' 5import { validateHost } from '../../../shared'
9import { FollowService } from '../shared' 6import { FollowService } from '../shared'
@@ -13,9 +10,9 @@ import { FollowService } from '../shared'
13 templateUrl: './following-add.component.html', 10 templateUrl: './following-add.component.html',
14 styleUrls: [ './following-add.component.scss' ] 11 styleUrls: [ './following-add.component.scss' ]
15}) 12})
16export class FollowingAddComponent implements OnInit { 13export class FollowingAddComponent {
17 form: FormGroup 14 hostsString = ''
18 hosts: string[] = [ ] 15 hostsError: string = null
19 error: string = null 16 error: string = null
20 17
21 constructor ( 18 constructor (
@@ -25,76 +22,50 @@ export class FollowingAddComponent implements OnInit {
25 private followService: FollowService 22 private followService: FollowService
26 ) {} 23 ) {}
27 24
28 ngOnInit () { 25 httpEnabled () {
29 this.form = new FormGroup({})
30 this.addField()
31 }
32
33 addField () {
34 this.form.addControl(`host-${this.hosts.length}`, new FormControl('', [ validateHost ]))
35 this.hosts.push('')
36 }
37
38 canMakeFriends () {
39 return window.location.protocol === 'https:' 26 return window.location.protocol === 'https:'
40 } 27 }
41 28
42 customTrackBy (index: number, obj: any): any { 29 onHostsChanged () {
43 return index 30 this.hostsError = null
44 }
45
46 displayAddField (index: number) {
47 return index === (this.hosts.length - 1)
48 }
49 31
50 displayRemoveField (index: number) { 32 const newHostsErrors = []
51 return (index !== 0 || this.hosts.length > 1) && index !== (this.hosts.length - 1) 33 const hosts = this.getNotEmptyHosts()
52 }
53 34
54 isFormValid () { 35 for (const host of hosts) {
55 // Do not check the last input 36 if (validateHost(host) === false) {
56 for (let i = 0; i < this.hosts.length - 1; i++) { 37 newHostsErrors.push(`${host} is not valid`)
57 if (!this.form.controls[`host-${i}`].valid) return false 38 }
58 } 39 }
59 40
60 const lastIndex = this.hosts.length - 1 41 if (newHostsErrors.length !== 0) {
61 // If the last input (which is not the first) is empty, it's ok 42 this.hostsError = newHostsErrors.join('. ')
62 if (this.hosts[lastIndex] === '' && lastIndex !== 0) {
63 return true
64 } else {
65 return this.form.controls[`host-${lastIndex}`].valid
66 } 43 }
67 } 44 }
68 45
69 removeField (index: number) {
70 // Remove the last control
71 this.form.removeControl(`host-${this.hosts.length - 1}`)
72 this.hosts.splice(index, 1)
73 }
74
75 addFollowing () { 46 addFollowing () {
76 this.error = '' 47 this.error = ''
77 48
78 const notEmptyHosts = this.getNotEmptyHosts() 49 const hosts = this.getNotEmptyHosts()
79 if (notEmptyHosts.length === 0) { 50 if (hosts.length === 0) {
80 this.error = 'You need to specify at least 1 host.' 51 this.error = 'You need to specify hosts to follow.'
81 return
82 } 52 }
83 53
84 if (!this.isHostsUnique(notEmptyHosts)) { 54 if (!this.isHostsUnique(hosts)) {
85 this.error = 'Hosts need to be unique.' 55 this.error = 'Hosts need to be unique.'
86 return 56 return
87 } 57 }
88 58
89 const confirmMessage = 'Are you sure to make friends with:<br /> - ' + notEmptyHosts.join('<br /> - ') 59 const confirmMessage = 'If you confirm, you will send a follow request to:<br /> - ' + hosts.join('<br /> - ')
90 this.confirmService.confirm(confirmMessage, 'Follow new server(s)').subscribe( 60 this.confirmService.confirm(confirmMessage, 'Follow new server(s)').subscribe(
91 res => { 61 res => {
92 if (res === false) return 62 if (res === false) return
93 63
94 this.followService.follow(notEmptyHosts).subscribe( 64 this.followService.follow(hosts).subscribe(
95 status => { 65 status => {
96 this.notificationsService.success('Success', 'Follow request(s) sent!') 66 this.notificationsService.success('Success', 'Follow request(s) sent!')
97 this.router.navigate([ '/admin/follows/following-list' ]) 67
68 setTimeout(() => this.router.navigate([ '/admin/follows/following-list' ]), 500)
98 }, 69 },
99 70
100 err => this.notificationsService.error('Error', err.message) 71 err => this.notificationsService.error('Error', err.message)
@@ -103,18 +74,15 @@ export class FollowingAddComponent implements OnInit {
103 ) 74 )
104 } 75 }
105 76
106 private getNotEmptyHosts () {
107 const notEmptyHosts = []
108
109 Object.keys(this.form.value).forEach((hostKey) => {
110 const host = this.form.value[hostKey]
111 if (host !== '') notEmptyHosts.push(host)
112 })
113
114 return notEmptyHosts
115 }
116
117 private isHostsUnique (hosts: string[]) { 77 private isHostsUnique (hosts: string[]) {
118 return hosts.every(host => hosts.indexOf(host) === hosts.lastIndexOf(host)) 78 return hosts.every(host => hosts.indexOf(host) === hosts.lastIndexOf(host))
119 } 79 }
80
81 private getNotEmptyHosts () {
82 const hosts = this.hostsString
83 .split('\n')
84 .filter(host => host && host.length !== 0) // Eject empty hosts
85
86 return hosts
87 }
120} 88}
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.html b/client/src/app/+admin/follows/following-list/following-list.component.html
index a73084312..2b6cc9113 100644
--- a/client/src/app/+admin/follows/following-list/following-list.component.html
+++ b/client/src/app/+admin/follows/following-list/following-list.component.html
@@ -1,20 +1,14 @@
1<div class="row"> 1<p-dataTable
2 <div class="content-padding"> 2 [value]="following" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
3 <h3>Following list</h3> 3 sortField="createdAt" (onLazyLoad)="loadLazy($event)"
4 4>
5 <p-dataTable 5 <p-column field="id" header="ID"></p-column>
6 [value]="following" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" 6 <p-column field="following.host" header="Host"></p-column>
7 sortField="createdAt" (onLazyLoad)="loadLazy($event)" 7 <p-column field="state" header="State"></p-column>
8 > 8 <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
9 <p-column field="id" header="ID"></p-column> 9 <p-column styleClass="action-cell">
10 <p-column field="following.host" header="Host"></p-column> 10 <ng-template pTemplate="body" let-following="rowData">
11 <p-column field="state" header="State"></p-column> 11 <my-delete-button (click)="removeFollowing(following)"></my-delete-button>
12 <p-column field="createdAt" header="Created date" [sortable]="true"></p-column> 12 </ng-template>
13 <p-column header="Unfollow" styleClass="action-cell"> 13 </p-column>
14 <ng-template pTemplate="body" let-following="rowData"> 14</p-dataTable>
15 <span (click)="removeFollowing(following)" class="glyphicon glyphicon-remove glyphicon-black" title="Unfollow"></span>
16 </ng-template>
17 </p-column>
18 </p-dataTable>
19 </div>
20</div>
diff --git a/client/src/app/+admin/follows/follows.component.html b/client/src/app/+admin/follows/follows.component.html
index b67bc9736..1baba5a4d 100644
--- a/client/src/app/+admin/follows/follows.component.html
+++ b/client/src/app/+admin/follows/follows.component.html
@@ -1,4 +1,6 @@
1<div class="follows-menu"> 1<div class="admin-sub-header">
2 <div class="admin-sub-title">Manage follows</div>
3
2 <tabset #followsMenuTabs> 4 <tabset #followsMenuTabs>
3 <tab *ngFor="let link of links"> 5 <tab *ngFor="let link of links">
4 <ng-template tabHeading> 6 <ng-template tabHeading>
@@ -8,4 +10,6 @@
8 </tabset> 10 </tabset>
9</div> 11</div>
10 12
13
14
11<router-outlet></router-outlet> 15<router-outlet></router-outlet>
diff --git a/client/src/app/+admin/follows/follows.component.scss b/client/src/app/+admin/follows/follows.component.scss
index d8ab41975..835fa3b78 100644
--- a/client/src/app/+admin/follows/follows.component.scss
+++ b/client/src/app/+admin/follows/follows.component.scss
@@ -1,21 +1,4 @@
1.follows-menu { 1.admin-sub-title {
2 margin-top: 20px; 2 flex-grow: 0;
3} 3 margin-right: 30px;
4
5tabset /deep/ {
6 .nav-link {
7 padding: 0;
8 }
9
10 .tab-link {
11 display: block;
12 text-align: center;
13 height: 40px;
14 width: 120px;
15 line-height: 40px;
16
17 &:hover, &:active, &:focus {
18 text-decoration: none !important;
19 }
20 }
21} 4}
diff --git a/client/src/app/+admin/follows/follows.component.ts b/client/src/app/+admin/follows/follows.component.ts
index a1be82585..f29ad384f 100644
--- a/client/src/app/+admin/follows/follows.component.ts
+++ b/client/src/app/+admin/follows/follows.component.ts
@@ -47,7 +47,7 @@ export class FollowsComponent implements OnInit, AfterViewInit {
47 for (let i = 0; i < this.links.length; i++) { 47 for (let i = 0; i < this.links.length; i++) {
48 const path = this.links[i].path 48 const path = this.links[i].path
49 49
50 if (url.endsWith(path) === true) { 50 if (url.endsWith(path) === true && this.followsMenuTabs.tabs[i]) {
51 this.followsMenuTabs.tabs[i].active = true 51 this.followsMenuTabs.tabs[i].active = true
52 return 52 return
53 } 53 }
diff --git a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.html b/client/src/app/+admin/jobs/jobs-list/jobs-list.component.html
index a90267172..7aa5f4254 100644
--- a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.html
+++ b/client/src/app/+admin/jobs/jobs-list/jobs-list.component.html
@@ -1,18 +1,20 @@
1<div class="row"> 1<div class="admin-sub-header">
2 <div class="content-padding"> 2 <div class="admin-sub-title">Jobs list</div>
3 <h3>Jobs list</h3>
4
5 <p-dataTable
6 [value]="jobs" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
7 sortField="createdAt" (onLazyLoad)="loadLazy($event)"
8 >
9 <p-column field="id" header="ID"></p-column>
10 <p-column field="category" header="Category"></p-column>
11 <p-column field="handlerName" header="Handler name"></p-column>
12 <p-column field="handlerInputData" header="Input data"></p-column>
13 <p-column field="state" header="State"></p-column>
14 <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
15 <p-column field="updatedAt" header="Updated date"></p-column>
16 </p-dataTable>
17 </div>
18</div> 3</div>
4
5<p-dataTable
6 [value]="jobs" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
7 sortField="createdAt" (onLazyLoad)="loadLazy($event)" [scrollable]="true" [virtualScroll]="true" [scrollHeight]="scrollHeight"
8>
9 <p-column field="id" header="ID" [style]="{ width: '40px' }"></p-column>
10 <p-column field="category" header="Category" [style]="{ width: '100px' }"></p-column>
11 <p-column field="handlerName" header="Handler name" [style]="{ width: '200px' }"></p-column>
12 <p-column header="Input data">
13 <ng-template pTemplate="body" let-job="rowData">
14 <pre>{{ job.handlerInputData }}</pre>
15 </ng-template>
16 </p-column>
17 <p-column field="state" header="State" [style]="{ width: '100px' }"></p-column>
18 <p-column field="createdAt" header="Created date" [sortable]="true" [style]="{ width: '250px' }"></p-column>
19 <p-column field="updatedAt" header="Updated date" [style]="{ width: '250px' }"></p-column>
20</p-dataTable>
diff --git a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.scss b/client/src/app/+admin/jobs/jobs-list/jobs-list.component.scss
new file mode 100644
index 000000000..9dde13216
--- /dev/null
+++ b/client/src/app/+admin/jobs/jobs-list/jobs-list.component.scss
@@ -0,0 +1,3 @@
1pre {
2 font-size: 13px;
3}
diff --git a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts b/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts
index 88fe259fb..f93847f29 100644
--- a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts
+++ b/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts
@@ -1,22 +1,24 @@
1import { Component } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { NotificationsService } from 'angular2-notifications'
3import { SortMeta } from 'primeng/primeng' 3import { SortMeta } from 'primeng/primeng'
4import { Job } from '../../../../../../shared/index' 4import { Job } from '../../../../../../shared/index'
5import { RestPagination, RestTable } from '../../../shared' 5import { RestPagination, RestTable } from '../../../shared'
6import { viewportHeight } from '../../../shared/misc/utils'
6import { JobService } from '../shared' 7import { JobService } from '../shared'
7import { RestExtractor } from '../../../shared/rest/rest-extractor.service' 8import { RestExtractor } from '../../../shared/rest/rest-extractor.service'
8 9
9@Component({ 10@Component({
10 selector: 'my-jobs-list', 11 selector: 'my-jobs-list',
11 templateUrl: './jobs-list.component.html', 12 templateUrl: './jobs-list.component.html',
12 styleUrls: [ ] 13 styleUrls: [ './jobs-list.component.scss' ]
13}) 14})
14export class JobsListComponent extends RestTable { 15export class JobsListComponent extends RestTable implements OnInit {
15 jobs: Job[] = [] 16 jobs: Job[] = []
16 totalRecords = 0 17 totalRecords = 0
17 rowsPerPage = 10 18 rowsPerPage = 20
18 sort: SortMeta = { field: 'createdAt', order: 1 } 19 sort: SortMeta = { field: 'createdAt', order: 1 }
19 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 20 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
21 scrollHeight = ''
20 22
21 constructor ( 23 constructor (
22 private notificationsService: NotificationsService, 24 private notificationsService: NotificationsService,
@@ -26,10 +28,14 @@ export class JobsListComponent extends RestTable {
26 super() 28 super()
27 } 29 }
28 30
31 ngOnInit () {
32 // 270 -> headers + footer...
33 this.scrollHeight = (viewportHeight() - 380) + 'px'
34 }
35
29 protected loadData () { 36 protected loadData () {
30 this.jobsService 37 this.jobsService
31 .getJobs(this.pagination, this.sort) 38 .getJobs(this.pagination, this.sort)
32 .map(res => this.restExtractor.applyToResultListData(res, this.formatJob.bind(this)))
33 .subscribe( 39 .subscribe(
34 resultList => { 40 resultList => {
35 this.jobs = resultList.data 41 this.jobs = resultList.data
@@ -39,12 +45,4 @@ export class JobsListComponent extends RestTable {
39 err => this.notificationsService.error('Error', err.message) 45 err => this.notificationsService.error('Error', err.message)
40 ) 46 )
41 } 47 }
42
43 private formatJob (job: Job) {
44 const handlerInputData = JSON.stringify(job.handlerInputData)
45
46 return Object.assign(job, {
47 handlerInputData
48 })
49 }
50} 48}
diff --git a/client/src/app/+admin/jobs/shared/job.service.ts b/client/src/app/+admin/jobs/shared/job.service.ts
index 49f1ab6f5..0cfbdbbea 100644
--- a/client/src/app/+admin/jobs/shared/job.service.ts
+++ b/client/src/app/+admin/jobs/shared/job.service.ts
@@ -25,6 +25,13 @@ export class JobService {
25 25
26 return this.authHttp.get<ResultList<Job>>(JobService.BASE_JOB_URL, { params }) 26 return this.authHttp.get<ResultList<Job>>(JobService.BASE_JOB_URL, { params })
27 .map(res => this.restExtractor.convertResultListDateToHuman(res)) 27 .map(res => this.restExtractor.convertResultListDateToHuman(res))
28 .map(res => this.restExtractor.applyToResultListData(res, this.prettyPrintData))
28 .catch(err => this.restExtractor.handleError(err)) 29 .catch(err => this.restExtractor.handleError(err))
29 } 30 }
31
32 private prettyPrintData (obj: Job) {
33 const handlerInputData = JSON.stringify(obj.handlerInputData, null, 2)
34
35 return Object.assign(obj, { handlerInputData })
36 }
30} 37}
diff --git a/client/src/app/+admin/users/shared/user.service.ts b/client/src/app/+admin/users/shared/user.service.ts
index e4bd5df37..dc77cc1d8 100644
--- a/client/src/app/+admin/users/shared/user.service.ts
+++ b/client/src/app/+admin/users/shared/user.service.ts
@@ -1,14 +1,12 @@
1import { Injectable } from '@angular/core'
2import { HttpClient, HttpParams } from '@angular/common/http' 1import { HttpClient, HttpParams } from '@angular/common/http'
3import { Observable } from 'rxjs/Observable' 2import { Injectable } from '@angular/core'
3import { BytesPipe } from 'ngx-pipes'
4import { SortMeta } from 'primeng/components/common/sortmeta'
4import 'rxjs/add/operator/catch' 5import 'rxjs/add/operator/catch'
5import 'rxjs/add/operator/map' 6import 'rxjs/add/operator/map'
6 7import { Observable } from 'rxjs/Observable'
7import { SortMeta } from 'primeng/components/common/sortmeta' 8import { ResultList, UserCreate, UserUpdate } from '../../../../../../shared'
8import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe' 9import { RestExtractor, RestPagination, RestService, User } from '../../../shared'
9
10import { RestExtractor, User, RestPagination, RestService } from '../../../shared'
11import { UserCreate, UserUpdate, ResultList } from '../../../../../../shared'
12 10
13@Injectable() 11@Injectable()
14export class UserService { 12export class UserService {
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html
index 349be13c1..963e2f39a 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.component.html
+++ b/client/src/app/+admin/users/user-edit/user-edit.component.html
@@ -1,73 +1,68 @@
1<div class="row"> 1<div class="admin-sub-title" *ngIf="isCreation() === true">Add user</div>
2 <div class="content-padding"> 2<div class="admin-sub-title" *ngIf="isCreation() === false">Edit user {{ username }}</div>
3 3
4 <h3 *ngIf="isCreation() === true">Add user</h3> 4<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
5 <h3 *ngIf="isCreation() === false">Edit user {{ username }}</h3>
6 5
7 <div *ngIf="error" class="alert alert-danger">{{ error }}</div> 6<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
8 7 <div class="form-group" *ngIf="isCreation()">
9 <form role="form" (ngSubmit)="formValidated()" [formGroup]="form"> 8 <label for="username">Username</label>
10 <div class="form-group" *ngIf="isCreation()"> 9 <input
11 <label for="username">Username</label> 10 type="text" class="form-control" id="username" placeholder="john"
12 <input 11 formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }"
13 type="text" class="form-control" id="username" placeholder="john" 12 >
14 formControlName="username" 13 <div *ngIf="formErrors.username" class="form-error">
15 > 14 {{ formErrors.username }}
16 <div *ngIf="formErrors.username" class="alert alert-danger"> 15 </div>
17 {{ formErrors.username }} 16 </div>
18 </div>
19 </div>
20
21 <div class="form-group">
22 <label for="email">Email</label>
23 <input
24 type="text" class="form-control" id="email" placeholder="mail@example.com"
25 formControlName="email"
26 >
27 <div *ngIf="formErrors.email" class="alert alert-danger">
28 {{ formErrors.email }}
29 </div>
30 </div>
31 17
32 <div class="form-group" *ngIf="isCreation()"> 18 <div class="form-group">
33 <label for="password">Password</label> 19 <label for="email">Email</label>
34 <input 20 <input
35 type="password" class="form-control" id="password" 21 type="text" class="form-control" id="email" placeholder="mail@example.com"
36 formControlName="password" 22 formControlName="email" [ngClass]="{ 'input-error': formErrors['email'] }"
37 > 23 >
38 <div *ngIf="formErrors.password" class="alert alert-danger"> 24 <div *ngIf="formErrors.email" class="form-error">
39 {{ formErrors.password }} 25 {{ formErrors.email }}
40 </div> 26 </div>
41 </div> 27 </div>
42 28
43 <div class="form-group"> 29 <div class="form-group" *ngIf="isCreation()">
44 <label for="role">Role</label> 30 <label for="password">Password</label>
45 <select class="form-control" id="role" formControlName="role"> 31 <input
46 <option *ngFor="let role of roles" [value]="role.value"> 32 type="password" class="form-control" id="password"
47 {{ role.label }} 33 formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
48 </option> 34 >
49 </select> 35 <div *ngIf="formErrors.password" class="form-error">
36 {{ formErrors.password }}
37 </div>
38 </div>
50 39
51 <div *ngIf="formErrors.role" class="alert alert-danger"> 40 <div class="form-group">
52 {{ formErrors.role }} 41 <label for="role">Role</label>
53 </div> 42 <select class="form-control" id="role" formControlName="role">
54 </div> 43 <option *ngFor="let role of roles" [value]="role.value">
44 {{ role.label }}
45 </option>
46 </select>
55 47
56 <div class="form-group"> 48 <div *ngIf="formErrors.role" class="form-error">
57 <label for="videoQuota">Video quota</label> 49 {{ formErrors.role }}
58 <select class="form-control" id="videoQuota" formControlName="videoQuota"> 50 </div>
59 <option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value"> 51 </div>
60 {{ videoQuotaOption.label }}
61 </option>
62 </select>
63 52
64 <div class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()"> 53 <div class="form-group">
65 Transcoding is enabled on server. The video quota only take in account <strong>original</strong> video. <br /> 54 <label for="videoQuota">Video quota</label>
66 In maximum, this user could use ~ {{ computeQuotaWithTranscoding() | bytes }}. 55 <select class="form-control" id="videoQuota" formControlName="videoQuota">
67 </div> 56 <option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value">
68 </div> 57 {{ videoQuotaOption.label }}
58 </option>
59 </select>
69 60
70 <input type="submit" value="{{ getFormButtonTitle() }}" class="btn btn-default" [disabled]="!form.valid"> 61 <div class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()">
71 </form> 62 Transcoding is enabled on server. The video quota only take in account <strong>original</strong> video. <br />
63 In maximum, this user could use ~ {{ computeQuotaWithTranscoding() | bytes }}.
64 </div>
72 </div> 65 </div>
73</div> 66
67 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
68</form>
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.scss b/client/src/app/+admin/users/user-edit/user-edit.component.scss
index 401caa0c6..68d270c19 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.component.scss
+++ b/client/src/app/+admin/users/user-edit/user-edit.component.scss
@@ -1,3 +1,21 @@
1.admin-sub-title {
2 margin-bottom: 30px;
3}
4
5input:not([type=submit]) {
6 @include peertube-input-text(340px);
7 display: block;
8}
9
10select {
11 @include peertube-select(340px);
12}
13
14input[type=submit] {
15 @include peertube-button;
16 @include orange-button;
17}
18
1.transcoding-information { 19.transcoding-information {
2 margin-top: 5px; 20 margin-top: 5px;
3 font-size: 11px; 21 font-size: 11px;
diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html
index 16a8a8033..b3d90ba1e 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.html
+++ b/client/src/app/+admin/users/user-list/user-list.component.html
@@ -1,35 +1,26 @@
1<div class="row"> 1<div class="admin-sub-header">
2 <div class="content-padding"> 2 <div class="admin-sub-title">Users list</div>
3 3
4 <h3>Users list</h3> 4 <a class="add-button" routerLink="/admin/users/add">
5 5 <span class="icon icon-add"></span>
6 <p-dataTable 6 Add user
7 [value]="users" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" 7 </a>
8 sortField="id" (onLazyLoad)="loadLazy($event)"
9 >
10 <p-column field="id" header="ID" [sortable]="true"></p-column>
11 <p-column field="username" header="Username" [sortable]="true"></p-column>
12 <p-column field="email" header="Email"></p-column>
13 <p-column field="videoQuota" header="Video quota"></p-column>
14 <p-column field="roleLabel" header="Role"></p-column>
15 <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
16 <p-column header="Edit" styleClass="action-cell">
17 <ng-template pTemplate="body" let-user="rowData">
18 <a [routerLink]="getRouterUserEditLink(user)" title="Edit this user">
19 <span class="glyphicon glyphicon-pencil glyphicon-black"></span>
20 </a>
21 </ng-template>
22 </p-column>
23 <p-column header="Delete" styleClass="action-cell">
24 <ng-template pTemplate="body" let-user="rowData">
25 <span (click)="removeUser(user)" class="glyphicon glyphicon-remove glyphicon-black" title="Remove this user"></span>
26 </ng-template>
27 </p-column>
28 </p-dataTable>
29
30 <a class="add-user btn btn-success pull-right" [routerLink]="['/admin/users/add']">
31 <span class="glyphicon glyphicon-plus"></span>
32 Add user
33 </a>
34 </div>
35</div> 8</div>
9
10<p-dataTable
11 [value]="users" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
12 sortField="id" (onLazyLoad)="loadLazy($event)"
13>
14 <p-column field="id" header="ID" [sortable]="true"></p-column>
15 <p-column field="username" header="Username" [sortable]="true"></p-column>
16 <p-column field="email" header="Email"></p-column>
17 <p-column field="videoQuota" header="Video quota"></p-column>
18 <p-column field="roleLabel" header="Role"></p-column>
19 <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
20 <p-column styleClass="action-cell">
21 <ng-template pTemplate="body" let-user="rowData">
22 <my-edit-button [routerLink]="getRouterUserEditLink(user)"></my-edit-button>
23 <my-delete-button (click)="removeUser(user)"></my-delete-button>
24 </ng-template>
25 </p-column>
26</p-dataTable>
diff --git a/client/src/app/+admin/users/user-list/user-list.component.scss b/client/src/app/+admin/users/user-list/user-list.component.scss
index 71adef653..8b22f67ff 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.scss
+++ b/client/src/app/+admin/users/user-list/user-list.component.scss
@@ -1,3 +1,11 @@
1.add-user { 1 .add-button {
2 margin-top: 10px; 2 @include peertube-button-link;
3} 3 @include orange-button;
4
5 .icon.icon-add {
6 @include icon(22px);
7
8 margin-right: 3px;
9 background-image: url('../../../../assets/images/admin/add.svg');
10 }
11 }
diff --git a/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html
index ab0a9d99f..d655a5e9b 100644
--- a/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html
+++ b/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html
@@ -1,24 +1,19 @@
1<div class="row"> 1<div class="admin-sub-header">
2 <div class="content-padding"> 2 <div class="admin-sub-title">Video abuses list</div>
3
4 <h3>Video abuses list</h3>
5
6 <p-dataTable
7 [value]="videoAbuses" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
8 sortField="id" (onLazyLoad)="loadLazy($event)"
9 >
10 <p-column field="id" header="ID" [sortable]="true"></p-column>
11 <p-column field="reason" header="Reason"></p-column>
12 <p-column field="reporterServerHost" header="Reporter server host"></p-column>
13 <p-column field="reporterUsername" header="Reporter username"></p-column>
14 <p-column field="videoName" header="Video name"></p-column>
15 <p-column header="Video" styleClass="action-cell">
16 <ng-template pTemplate="body" let-videoAbuse="rowData">
17 <a [routerLink]="getRouterVideoLink(videoAbuse.videoId)" title="Go to the video">{{ videoAbuse.videoId }}</a>
18 </ng-template>
19 </p-column>
20 <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
21 </p-dataTable>
22
23 </div>
24</div> 3</div>
4
5<p-dataTable
6 [value]="videoAbuses" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
7 sortField="id" (onLazyLoad)="loadLazy($event)"
8>
9 <p-column field="id" header="ID" [sortable]="true"></p-column>
10 <p-column field="reason" header="Reason"></p-column>
11 <p-column field="reporterServerHost" header="Reporter server host"></p-column>
12 <p-column field="reporterUsername" header="Reporter username"></p-column>
13 <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
14 <p-column header="Video">
15 <ng-template pTemplate="body" let-videoAbuse="rowData">
16 <a [routerLink]="getRouterVideoLink(videoAbuse.videoId)" title="Go to the video">{{ videoAbuse.videoName }}</a>
17 </ng-template>
18 </p-column>
19</p-dataTable>
diff --git a/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.scss b/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.scss
new file mode 100644
index 000000000..6a4762650
--- /dev/null
+++ b/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.scss
@@ -0,0 +1,6 @@
1/deep/ a {
2
3 &, &:hover, &:active, &:focus {
4 color: #000;
5 }
6}
diff --git a/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.ts b/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.ts
index 654603d01..b4d3bbd24 100644
--- a/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.ts
+++ b/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.ts
@@ -8,7 +8,8 @@ import { VideoAbuse } from '../../../../../../shared'
8 8
9@Component({ 9@Component({
10 selector: 'my-video-abuse-list', 10 selector: 'my-video-abuse-list',
11 templateUrl: './video-abuse-list.component.html' 11 templateUrl: './video-abuse-list.component.html',
12 styleUrls: [ './video-abuse-list.component.scss']
12}) 13})
13export class VideoAbuseListComponent extends RestTable implements OnInit { 14export class VideoAbuseListComponent extends RestTable implements OnInit {
14 videoAbuses: VideoAbuse[] = [] 15 videoAbuses: VideoAbuse[] = []
diff --git a/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.html b/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.html
index 05d116798..1d813fa07 100644
--- a/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.html
+++ b/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.html
@@ -18,7 +18,7 @@
18 <p-column field="createdAt" header="Created date" [sortable]="true"></p-column> 18 <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
19 <p-column header="Delete" styleClass="action-cell"> 19 <p-column header="Delete" styleClass="action-cell">
20 <ng-template pTemplate="body" let-entry="rowData"> 20 <ng-template pTemplate="body" let-entry="rowData">
21 <span (click)="removeVideoFromBlacklist(entry)" class="glyphicon glyphicon-remove glyphicon-black" title="Remove this video from blacklist"></span> 21 <my-delete-button (click)="removeVideoFromBlacklist(entry)"></my-delete-button>
22 </ng-template> 22 </ng-template>
23 </p-column> 23 </p-column>
24 </p-dataTable> 24 </p-dataTable>
diff --git a/client/src/app/account/account-change-password/account-change-password.component.html b/client/src/app/account/account-change-password/account-change-password.component.html
deleted file mode 100644
index 92d9f900a..000000000
--- a/client/src/app/account/account-change-password/account-change-password.component.html
+++ /dev/null
@@ -1,24 +0,0 @@
1<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
2
3<form role="form" (ngSubmit)="changePassword()" [formGroup]="form">
4 <div class="form-group">
5 <label for="new-password">New password</label>
6 <input
7 type="password" class="form-control" id="new-password"
8 formControlName="new-password"
9 >
10 <div *ngIf="formErrors['new-password']" class="alert alert-danger">
11 {{ formErrors['new-password'] }}
12 </div>
13 </div>
14
15 <div class="form-group">
16 <label for="name">Confirm new password</label>
17 <input
18 type="password" class="form-control" id="new-confirmed-password"
19 formControlName="new-confirmed-password"
20 >
21 </div>
22
23 <input type="submit" value="Change password" class="btn btn-default" [disabled]="!form.valid">
24</form>
diff --git a/client/src/app/account/account-details/account-details.component.html b/client/src/app/account/account-details/account-details.component.html
deleted file mode 100644
index 8f4f176af..000000000
--- a/client/src/app/account/account-details/account-details.component.html
+++ /dev/null
@@ -1,16 +0,0 @@
1<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
2
3<form role="form" (ngSubmit)="updateDetails()" [formGroup]="form">
4 <div class="form-group">
5 <input
6 type="checkbox" id="displayNSFW"
7 formControlName="displayNSFW"
8 >
9 <label for="displayNSFW">Display videos that contain mature or explicit content</label>
10 <div *ngIf="formErrors['displayNSFW']" class="alert alert-danger">
11 {{ formErrors['displayNSFW'] }}
12 </div>
13 </div>
14
15 <input type="submit" value="Update" class="btn btn-default" [disabled]="!form.valid">
16</form>
diff --git a/client/src/app/account/account-routing.module.ts b/client/src/app/account/account-routing.module.ts
index 74d9aa03e..070b9b5c5 100644
--- a/client/src/app/account/account-routing.module.ts
+++ b/client/src/app/account/account-routing.module.ts
@@ -5,17 +5,34 @@ import { MetaGuard } from '@ngx-meta/core'
5 5
6import { LoginGuard } from '../core' 6import { LoginGuard } from '../core'
7import { AccountComponent } from './account.component' 7import { AccountComponent } from './account.component'
8import { AccountSettingsComponent } from './account-settings/account-settings.component'
9import { AccountVideosComponent } from './account-videos/account-videos.component'
8 10
9const accountRoutes: Routes = [ 11const accountRoutes: Routes = [
10 { 12 {
11 path: 'account', 13 path: 'account',
12 component: AccountComponent, 14 component: AccountComponent,
13 canActivate: [ MetaGuard, LoginGuard ], 15 canActivateChild: [ MetaGuard, LoginGuard ],
14 data: { 16 children: [
15 meta: { 17 {
16 title: 'My account' 18 path: 'settings',
19 component: AccountSettingsComponent,
20 data: {
21 meta: {
22 title: 'Account settings'
23 }
24 }
25 },
26 {
27 path: 'videos',
28 component: AccountVideosComponent,
29 data: {
30 meta: {
31 title: 'Account videos'
32 }
33 }
17 } 34 }
18 } 35 ]
19 } 36 }
20] 37]
21 38
diff --git a/client/src/app/account/account-settings/account-change-password/account-change-password.component.html b/client/src/app/account/account-settings/account-change-password/account-change-password.component.html
new file mode 100644
index 000000000..b0e3cada4
--- /dev/null
+++ b/client/src/app/account/account-settings/account-change-password/account-change-password.component.html
@@ -0,0 +1,20 @@
1<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
2
3<form role="form" (ngSubmit)="changePassword()" [formGroup]="form">
4
5 <label for="new-password">Change password</label>
6 <input
7 type="password" id="new-password" placeholder="New password"
8 formControlName="new-password" [ngClass]="{ 'input-error': formErrors['new-password'] }"
9 >
10 <div *ngIf="formErrors['new-password']" class="form-error">
11 {{ formErrors['new-password'] }}
12 </div>
13
14 <input
15 type="password" id="new-confirmed-password" placeholder="Confirm new password"
16 formControlName="new-confirmed-password"
17 >
18
19 <input type="submit" value="Change password" [disabled]="!form.valid">
20</form>
diff --git a/client/src/app/account/account-settings/account-change-password/account-change-password.component.scss b/client/src/app/account/account-settings/account-change-password/account-change-password.component.scss
new file mode 100644
index 000000000..7a4fdb34d
--- /dev/null
+++ b/client/src/app/account/account-settings/account-change-password/account-change-password.component.scss
@@ -0,0 +1,16 @@
1input[type=password] {
2 @include peertube-input-text(340px);
3 display: block;
4
5 &#new-confirmed-password {
6 margin-top: 15px;
7 }
8}
9
10input[type=submit] {
11 @include peertube-button;
12 @include orange-button;
13
14 margin-top: 15px;
15}
16
diff --git a/client/src/app/account/account-change-password/account-change-password.component.ts b/client/src/app/account/account-settings/account-change-password/account-change-password.component.ts
index 69edec54b..8979e1734 100644
--- a/client/src/app/account/account-change-password/account-change-password.component.ts
+++ b/client/src/app/account/account-settings/account-change-password/account-change-password.component.ts
@@ -1,16 +1,13 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { FormBuilder, FormGroup } from '@angular/forms' 2import { FormBuilder, FormGroup } from '@angular/forms'
3import { Router } from '@angular/router'
4
5import { NotificationsService } from 'angular2-notifications' 3import { NotificationsService } from 'angular2-notifications'
6 4import { FormReactive, USER_PASSWORD, UserService } from '../../../shared'
7import { FormReactive, UserService, USER_PASSWORD } from '../../shared'
8 5
9@Component({ 6@Component({
10 selector: 'my-account-change-password', 7 selector: 'my-account-change-password',
11 templateUrl: './account-change-password.component.html' 8 templateUrl: './account-change-password.component.html',
9 styleUrls: [ './account-change-password.component.scss' ]
12}) 10})
13
14export class AccountChangePasswordComponent extends FormReactive implements OnInit { 11export class AccountChangePasswordComponent extends FormReactive implements OnInit {
15 error: string = null 12 error: string = null
16 13
diff --git a/client/src/app/account/account-change-password/index.ts b/client/src/app/account/account-settings/account-change-password/index.ts
index 44c330b66..44c330b66 100644
--- a/client/src/app/account/account-change-password/index.ts
+++ b/client/src/app/account/account-settings/account-change-password/index.ts
diff --git a/client/src/app/account/account-settings/account-details/account-details.component.html b/client/src/app/account/account-settings/account-details/account-details.component.html
new file mode 100644
index 000000000..bc18b39b4
--- /dev/null
+++ b/client/src/app/account/account-settings/account-details/account-details.component.html
@@ -0,0 +1,14 @@
1<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
2
3<form role="form" (ngSubmit)="updateDetails()" [formGroup]="form">
4 <input
5 type="checkbox" id="displayNSFW"
6 formControlName="displayNSFW"
7 >
8 <label for="displayNSFW">Display videos that contain mature or explicit content</label>
9 <div *ngIf="formErrors['displayNSFW']" class="alert alert-danger">
10 {{ formErrors['displayNSFW'] }}
11 </div>
12
13 <input type="submit" value="Save" [disabled]="!form.valid">
14</form>
diff --git a/client/src/app/account/account-settings/account-details/account-details.component.scss b/client/src/app/account/account-settings/account-details/account-details.component.scss
new file mode 100644
index 000000000..5c369f968
--- /dev/null
+++ b/client/src/app/account/account-settings/account-details/account-details.component.scss
@@ -0,0 +1,13 @@
1label {
2 font-size: 15px;
3 font-weight: $font-regular;
4 margin-left: 5px;
5}
6
7input[type=submit] {
8 @include peertube-button;
9 @include orange-button;
10
11 display: block;
12 margin-top: 15px;
13}
diff --git a/client/src/app/account/account-details/account-details.component.ts b/client/src/app/account/account-settings/account-details/account-details.component.ts
index d7a6e6871..d835c53e5 100644
--- a/client/src/app/account/account-details/account-details.component.ts
+++ b/client/src/app/account/account-settings/account-details/account-details.component.ts
@@ -1,21 +1,14 @@
1import { Component, OnInit, Input } from '@angular/core' 1import { Component, Input, OnInit } from '@angular/core'
2import { FormBuilder, FormGroup } from '@angular/forms' 2import { FormBuilder, FormGroup } from '@angular/forms'
3import { Router } from '@angular/router'
4
5import { NotificationsService } from 'angular2-notifications' 3import { NotificationsService } from 'angular2-notifications'
6 4import { UserUpdateMe } from '../../../../../../shared'
7import { AuthService } from '../../core' 5import { AuthService } from '../../../core'
8import { 6import { FormReactive, User, UserService } from '../../../shared'
9 FormReactive,
10 User,
11 UserService,
12 USER_PASSWORD
13} from '../../shared'
14import { UserUpdateMe } from '../../../../../shared'
15 7
16@Component({ 8@Component({
17 selector: 'my-account-details', 9 selector: 'my-account-details',
18 templateUrl: './account-details.component.html' 10 templateUrl: './account-details.component.html',
11 styleUrls: [ './account-details.component.scss' ]
19}) 12})
20 13
21export class AccountDetailsComponent extends FormReactive implements OnInit { 14export class AccountDetailsComponent extends FormReactive implements OnInit {
diff --git a/client/src/app/account/account-details/index.ts b/client/src/app/account/account-settings/account-details/index.ts
index 4829f608a..4829f608a 100644
--- a/client/src/app/account/account-details/index.ts
+++ b/client/src/app/account/account-settings/account-details/index.ts
diff --git a/client/src/app/account/account-settings/account-settings.component.html b/client/src/app/account/account-settings/account-settings.component.html
new file mode 100644
index 000000000..c0a74cc47
--- /dev/null
+++ b/client/src/app/account/account-settings/account-settings.component.html
@@ -0,0 +1,15 @@
1<div class="user">
2 <img [src]="getAvatarPath()" alt="Avatar" />
3
4 <div class="user-info">
5 <div class="user-info-username">{{ user.username }}</div>
6 <div class="user-info-followers">{{ user.account?.followersCount }} subscribers</div>
7 </div>
8</div>
9
10
11<div class="account-title">Account settings</div>
12<my-account-change-password></my-account-change-password>
13
14<div class="account-title">Filtering</div>
15<my-account-details [user]="user"></my-account-details>
diff --git a/client/src/app/account/account-settings/account-settings.component.scss b/client/src/app/account/account-settings/account-settings.component.scss
new file mode 100644
index 000000000..f514809b0
--- /dev/null
+++ b/client/src/app/account/account-settings/account-settings.component.scss
@@ -0,0 +1,28 @@
1.user {
2 display: flex;
3
4 img {
5 @include avatar(50px);
6 margin-right: 15px;
7 }
8
9 .user-info {
10 .user-info-username {
11 font-size: 20px;
12 font-weight: $font-bold;
13 }
14
15 .user-info-followers {
16 font-size: 15px;
17 }
18 }
19}
20
21.account-title {
22 text-transform: uppercase;
23 color: $orange-color;
24 font-weight: $font-bold;
25 font-size: 13px;
26 margin-top: 55px;
27 margin-bottom: 30px;
28}
diff --git a/client/src/app/account/account-settings/account-settings.component.ts b/client/src/app/account/account-settings/account-settings.component.ts
new file mode 100644
index 000000000..cba251000
--- /dev/null
+++ b/client/src/app/account/account-settings/account-settings.component.ts
@@ -0,0 +1,22 @@
1import { Component, OnInit } from '@angular/core'
2import { User } from '../../shared'
3import { AuthService } from '../../core'
4
5@Component({
6 selector: 'my-account-settings',
7 templateUrl: './account-settings.component.html',
8 styleUrls: [ './account-settings.component.scss' ]
9})
10export class AccountSettingsComponent implements OnInit {
11 user: User = null
12
13 constructor (private authService: AuthService) {}
14
15 ngOnInit () {
16 this.user = this.authService.getUser()
17 }
18
19 getAvatarPath () {
20 return this.user.getAvatarPath()
21 }
22}
diff --git a/client/src/app/account/account-videos/account-videos.component.html b/client/src/app/account/account-videos/account-videos.component.html
new file mode 100644
index 000000000..f69c0487d
--- /dev/null
+++ b/client/src/app/account/account-videos/account-videos.component.html
@@ -0,0 +1,39 @@
1<div
2 class="videos"
3 infiniteScroll
4 [infiniteScrollDistance]="0.5"
5 [infiniteScrollUpDistance]="1.5"
6 (scrolled)="onNearOfBottom()"
7 (scrolledUp)="onNearOfTop()"
8>
9 <div class="video" *ngFor="let video of videos; let i = index">
10 <input type="checkbox" [(ngModel)]="checkedVideos[video.id]" />
11
12 <my-video-thumbnail [video]="video"></my-video-thumbnail>
13
14 <div class="video-info">
15 <div class="video-info-name">{{ video.name }}</div>
16 <span class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
17 </div>
18
19 <!-- Display only once -->
20 <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0">
21 <div class="action-selection-mode-child">
22 <span class="action-button action-button-cancel-selection" (click)="abortSelectionMode()">
23 Cancel
24 </span>
25
26 <span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()">
27 <span class="icon icon-delete-white"></span>
28 Delete
29 </span>
30 </div>
31 </div>
32
33 <div class="video-buttons" *ngIf="isInSelectionMode() === false">
34 <my-delete-button (click)="deleteVideo(video)"></my-delete-button>
35
36 <my-edit-button [routerLink]="[ '/videos', 'edit', video.uuid ]"></my-edit-button>
37 </div>
38 </div>
39</div>
diff --git a/client/src/app/account/account-videos/account-videos.component.scss b/client/src/app/account/account-videos/account-videos.component.scss
new file mode 100644
index 000000000..5459014a6
--- /dev/null
+++ b/client/src/app/account/account-videos/account-videos.component.scss
@@ -0,0 +1,96 @@
1.action-selection-mode {
2 width: 174px;
3 display: flex;
4 justify-content: flex-end;
5
6 .action-selection-mode-child {
7 position: fixed;
8
9 .action-button {
10 display: inline-block;
11 }
12
13 .action-button-cancel-selection {
14 @include peertube-button;
15 @include grey-button;
16
17 margin-right: 10px;
18 }
19
20 .action-button-delete-selection {
21 @include peertube-button;
22 @include orange-button;
23 }
24
25 .icon.icon-delete-white {
26 @include icon(21px);
27
28 position: relative;
29 top: -2px;
30 background-image: url('../../../assets/images/global/delete-white.svg');
31 }
32 }
33}
34
35/deep/ .action-button {
36 &.action-button-delete {
37 margin-right: 10px;
38 }
39}
40
41.video {
42 display: flex;
43 height: 130px;
44 padding-bottom: 20px;
45
46 input[type=checkbox] {
47 margin-right: 20px;
48 outline: 0;
49 }
50
51 &:first-child {
52 margin-top: 47px;
53 }
54
55 &:not(:last-child) {
56 margin-bottom: 20px;
57 border-bottom: 1px solid #C6C6C6;
58 }
59
60 my-video-thumbnail {
61 margin-right: 10px;
62 }
63
64 .video-info {
65 flex-grow: 1;
66
67 .video-info-name {
68 font-size: 16px;
69 font-weight: $font-semibold;
70 }
71
72 .video-info-date-views {
73 font-size: 13px;
74 }
75 }
76}
77
78@media screen and (max-width: 800px) {
79 .video {
80 flex-direction: column;
81 height: auto;
82 text-align: center;
83
84 input[type=checkbox] {
85 display: none;
86 }
87
88 my-video-thumbnail {
89 margin-right: 0;
90 }
91
92 .video-buttons {
93 margin-top: 10px;
94 }
95 }
96}
diff --git a/client/src/app/account/account-videos/account-videos.component.ts b/client/src/app/account/account-videos/account-videos.component.ts
new file mode 100644
index 000000000..5f12cfce0
--- /dev/null
+++ b/client/src/app/account/account-videos/account-videos.component.ts
@@ -0,0 +1,97 @@
1import { Component, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { NotificationsService } from 'angular2-notifications'
4import 'rxjs/add/observable/from'
5import 'rxjs/add/operator/concatAll'
6import { Observable } from 'rxjs/Observable'
7import { ConfirmService } from '../../core/confirm'
8import { AbstractVideoList } from '../../shared/video/abstract-video-list'
9import { Video } from '../../shared/video/video.model'
10import { VideoService } from '../../shared/video/video.service'
11
12@Component({
13 selector: 'my-account-videos',
14 templateUrl: './account-videos.component.html',
15 styleUrls: [ './account-videos.component.scss' ]
16})
17export class AccountVideosComponent extends AbstractVideoList implements OnInit {
18 titlePage = 'My videos'
19 currentRoute = '/account/videos'
20 checkedVideos: { [ id: number ]: boolean } = {}
21
22 constructor (protected router: Router,
23 protected route: ActivatedRoute,
24 protected notificationsService: NotificationsService,
25 protected confirmService: ConfirmService,
26 private videoService: VideoService) {
27 super()
28 }
29
30 ngOnInit () {
31 super.ngOnInit()
32 }
33
34 abortSelectionMode () {
35 this.checkedVideos = {}
36 }
37
38 isInSelectionMode () {
39 return Object.keys(this.checkedVideos).some(k => this.checkedVideos[k] === true)
40 }
41
42 getVideosObservable () {
43 return this.videoService.getMyVideos(this.pagination, this.sort)
44 }
45
46 deleteSelectedVideos () {
47 const toDeleteVideosIds = Object.keys(this.checkedVideos)
48 .filter(k => this.checkedVideos[k] === true)
49 .map(k => parseInt(k, 10))
50
51 this.confirmService.confirm(`Do you really want to delete ${toDeleteVideosIds.length} videos?`, 'Delete').subscribe(
52 res => {
53 if (res === false) return
54
55 const observables: Observable<any>[] = []
56 for (const videoId of toDeleteVideosIds) {
57 const o = this.videoService
58 .removeVideo(videoId)
59 .do(() => this.spliceVideosById(videoId))
60
61 observables.push(o)
62 }
63
64 Observable.from(observables)
65 .concatAll()
66 .subscribe(
67 res => this.notificationsService.success('Success', `${toDeleteVideosIds.length} videos deleted.`),
68
69 err => this.notificationsService.error('Error', err.text)
70 )
71 }
72 )
73 }
74
75 deleteVideo (video: Video) {
76 this.confirmService.confirm(`Do you really want to delete ${video.name}?`, 'Delete').subscribe(
77 res => {
78 if (res === false) return
79
80 this.videoService.removeVideo(video.id)
81 .subscribe(
82 status => {
83 this.notificationsService.success('Success', `Video ${video.name} deleted.`)
84 this.spliceVideosById(video.id)
85 },
86
87 error => this.notificationsService.error('Error', error.text)
88 )
89 }
90 )
91 }
92
93 private spliceVideosById (id: number) {
94 const index = this.videos.findIndex(v => v.id === id)
95 this.videos.splice(index, 1)
96 }
97}
diff --git a/client/src/app/account/account.component.html b/client/src/app/account/account.component.html
index 177e54999..d82a4ca4d 100644
--- a/client/src/app/account/account.component.html
+++ b/client/src/app/account/account.component.html
@@ -1,25 +1,11 @@
1<div class="row"> 1<div class="row">
2 <div class="content-padding"> 2 <div class="sub-menu">
3 <h3>Account</h3> 3 <a routerLink="/account/settings" routerLinkActive="active" class="title-page">My account</a>
4 4
5 <div class="col-md-6 col-sm-12"> 5 <a routerLink="/account/videos" routerLinkActive="active" class="title-page">My videos</a>
6 <div class="panel panel-default"> 6 </div>
7 <div class="panel-heading">Change password</div>
8
9 <div class="panel-body">
10 <my-account-change-password></my-account-change-password>
11 </div>
12 </div>
13 </div>
14
15 <div class="col-md-6 col-sm-12">
16 <div class="panel panel-default">
17 <div class="panel-heading">Update my informations</div>
18 7
19 <div class="panel-body"> 8 <div class="margin-content">
20 <my-account-details [user]="user"></my-account-details> 9 <router-outlet></router-outlet>
21 </div>
22 </div>
23 </div>
24 </div> 10 </div>
25</div> 11</div>
diff --git a/client/src/app/account/account.component.scss b/client/src/app/account/account.component.scss
index 61b80d0a7..e69de29bb 100644
--- a/client/src/app/account/account.component.scss
+++ b/client/src/app/account/account.component.scss
@@ -1,3 +0,0 @@
1.panel {
2 margin-top: 40px;
3}
diff --git a/client/src/app/account/account.component.ts b/client/src/app/account/account.component.ts
index 929934f67..3d3677ab0 100644
--- a/client/src/app/account/account.component.ts
+++ b/client/src/app/account/account.component.ts
@@ -1,28 +1,8 @@
1import { Component, OnInit } from '@angular/core' 1import { Component } from '@angular/core'
2import { FormBuilder, FormGroup } from '@angular/forms'
3import { Router } from '@angular/router'
4
5import { NotificationsService } from 'angular2-notifications'
6
7import { AuthService } from '../core'
8import {
9 FormReactive,
10 User,
11 UserService,
12 USER_PASSWORD
13} from '../shared'
14 2
15@Component({ 3@Component({
16 selector: 'my-account', 4 selector: 'my-account',
17 templateUrl: './account.component.html', 5 templateUrl: './account.component.html',
18 styleUrls: [ './account.component.scss' ] 6 styleUrls: [ './account.component.scss' ]
19}) 7})
20export class AccountComponent implements OnInit { 8export class AccountComponent {}
21 user: User = null
22
23 constructor (private authService: AuthService) {}
24
25 ngOnInit () {
26 this.user = this.authService.getUser()
27 }
28}
diff --git a/client/src/app/account/account.module.ts b/client/src/app/account/account.module.ts
index 380e9d235..020199e23 100644
--- a/client/src/app/account/account.module.ts
+++ b/client/src/app/account/account.module.ts
@@ -1,11 +1,12 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2 2import { SharedModule } from '../shared'
3import { AccountRoutingModule } from './account-routing.module' 3import { AccountRoutingModule } from './account-routing.module'
4import { AccountChangePasswordComponent } from './account-settings/account-change-password/account-change-password.component'
5import { AccountDetailsComponent } from './account-settings/account-details/account-details.component'
6import { AccountSettingsComponent } from './account-settings/account-settings.component'
4import { AccountComponent } from './account.component' 7import { AccountComponent } from './account.component'
5import { AccountChangePasswordComponent } from './account-change-password'
6import { AccountDetailsComponent } from './account-details'
7import { AccountService } from './account.service' 8import { AccountService } from './account.service'
8import { SharedModule } from '../shared' 9import { AccountVideosComponent } from './account-videos/account-videos.component'
9 10
10@NgModule({ 11@NgModule({
11 imports: [ 12 imports: [
@@ -15,8 +16,10 @@ import { SharedModule } from '../shared'
15 16
16 declarations: [ 17 declarations: [
17 AccountComponent, 18 AccountComponent,
19 AccountSettingsComponent,
18 AccountChangePasswordComponent, 20 AccountChangePasswordComponent,
19 AccountDetailsComponent 21 AccountDetailsComponent,
22 AccountVideosComponent
20 ], 23 ],
21 24
22 exports: [ 25 exports: [
diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts
index 0f9484344..fe72c9181 100644
--- a/client/src/app/app-routing.module.ts
+++ b/client/src/app/app-routing.module.ts
@@ -6,7 +6,7 @@ import { PreloadSelectedModulesList } from './core'
6const routes: Routes = [ 6const routes: Routes = [
7 { 7 {
8 path: '', 8 path: '',
9 redirectTo: '/videos/list', 9 redirectTo: '/videos/trending',
10 pathMatch: 'full' 10 pathMatch: 'full'
11 }, 11 },
12 { 12 {
diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html
index 8a826e783..da4273dda 100644
--- a/client/src/app/app.component.html
+++ b/client/src/app/app.component.html
@@ -1,37 +1,26 @@
1<div class="container-fluid"> 1<div>
2 <div class="row header"> 2 <div class="header">
3 3
4 <div class="col-md-2 col-sm-3 col-xs-3 top-left-block" [ngClass]="{ 'border-bottom': isMenuDisplayed === false }"> 4 <div class="top-left-block" [ngClass]="{ 'border-bottom': isMenuDisplayed === false }">
5 <div class="hamburger-block" (click)="toggleMenu()"> 5 <span class="icon icon-menu" (click)="toggleMenu()"></span>
6 <span class="glyphicon glyphicon-menu-hamburger"></span>
7 </div>
8 6
9 <div id="peertube-title"> 7 <a id="peertube-title" [routerLink]="['/videos/list']" title="Homepage">
10 <a [routerLink]="['/videos/list']" title="Homepage"></a> 8 <span class="icon icon-logo"></span>
11 </div> 9 PeerTube
10 </a>
12 </div> 11 </div>
13 12
14 <!-- Used for the fixed title --> 13 <div class="header-right">
15 <div class="col-md-2 col-sm-3 col-xs-3 fake-title-block"></div> 14 <my-header></my-header>
16
17 <!-- We need to reset col-md-* because my-search is in fixed position -->
18 <my-search class="col-md-10 col-sm-9 col-xs-9"></my-search>
19 </div>
20
21 <div class="row">
22 <div class="col-md-2 col-sm-3 col-xs-3 title-menu-left">
23
24 <div class="title-menu-left-block menu">
25 <my-menu *ngIf="isMenuDisplayed && isInAdmin() === false"></my-menu>
26 <my-menu-admin *ngIf="isMenuDisplayed && isInAdmin() === true"></my-menu-admin>
27 </div>
28 </div> 15 </div>
16 </div>
29 17
30 <!-- Used for the fixed menu --> 18 <div class="sub-header-container">
31 <div class="fake-menu col-md-2 col-sm-3 col-xs-3"> 19 <div *ngIf="isMenuDisplayed" class="title-menu-left">
20 <my-menu></my-menu>
32 </div> 21 </div>
33 22
34 <div class="main-col" [ngClass]="getMainColClasses()"> 23 <div class="main-col container-fluid" [ngClass]="getMainColClasses()">
35 24
36 <div class="main-row"> 25 <div class="main-row">
37 <router-outlet></router-outlet> 26 <router-outlet></router-outlet>
diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss
index a656d5c29..008c6d1f0 100644
--- a/client/src/app/app.component.scss
+++ b/client/src/app/app.component.scss
@@ -2,10 +2,15 @@
2 min-height: calc(100vh - #{$header-height} - #{$footer-height} - #{$footer-margin}); 2 min-height: calc(100vh - #{$header-height} - #{$footer-height} - #{$footer-margin});
3} 3}
4 4
5.sub-header-container {
6 margin-top: $header-height;
7}
8
5.title-menu-left { 9.title-menu-left {
6 position: fixed; 10 position: fixed;
7 height: calc(100vh - #{$header-height}); 11 height: calc(100vh - #{$header-height});
8 padding: 0; 12 padding: 0;
13 width: $menu-width;
9 14
10 .title-menu-left-block.menu { 15 .title-menu-left-block.menu {
11 height: 100%; 16 height: 100%;
@@ -14,125 +19,62 @@
14 19
15.header { 20.header {
16 height: $header-height; 21 height: $header-height;
17 22 position: fixed;
18 .fake-title-block { 23 top: 0;
19 display: inline-block; 24 width: 100%;
20 } 25 background-color: #fff;
26 z-index: 1000;
27 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.16);
28 display: flex;
21 29
22 .top-left-block { 30 .top-left-block {
23 z-index: 100; 31 width: $menu-width;
24 background-color: #fff; 32 z-index: 1001;
25 border-right: 1px solid $header-border-color;
26 height: $header-height; 33 height: $header-height;
27 line-height: $header-height;
28 margin-top: 0;
29 margin-bottom: 0;
30 display: flex; 34 display: flex;
31 position: fixed; 35 align-items: center;
32 padding: 0;
33 36
34 &.border-bottom { 37 .icon {
35 border-bottom: 1px solid $header-border-color; 38 @include icon(22px);
36 }
37
38 .hamburger-block {
39 margin-right: 15px;
40 margin-left: 15px;
41 39
42 .glyphicon { 40 &.icon-menu {
43 cursor: pointer; 41 background-image: url('../assets/images/header/menu.svg');
44 position: relative; 42 margin: 0 18px 0 24px;
45 top: 4px;
46 } 43 }
47 } 44 }
48 45
49 #peertube-title { 46 #peertube-title {
50 a { 47 font-size: 20px;
51 color: inherit !important; 48 font-weight: $font-bold;
52 display: block; 49 color: inherit !important;
53 background: url('../assets/logo.png') no-repeat; 50 display: flex;
54 background-size: contain; 51 align-items: center;
55 background-position: center; 52
56 height: 100%; 53 @include disable-default-a-behaviour;
57 margin: auto; 54
58 width: 135px; 55 .icon.icon-logo {
59 56 display: inline-block;
60 &:hover { 57 background: url('../assets/images/logo.svg') no-repeat;
61 color: inherit !important; 58 width: 23px;
62 text-decoration: none !important; 59 height: 24px;
63 }
64 } 60 }
65 } 61 }
66 62
67 @media screen and (max-width: 500px) { 63 @media screen and (max-width: 500px) {
64 width: 70px;
65
68 #peertube-title { 66 #peertube-title {
69 display: none; 67 display: none;
70 } 68 }
71
72 .hamburger-block {
73 width: 100%;
74 text-align: center;
75 }
76 }
77
78 @media screen and (min-width: 500px) and (max-width: 600px) {
79 #peertube-title a {
80 width: 80px;
81 }
82 }
83
84 @media screen and (min-width: 600px) and (max-width: 700px) {
85 #peertube-title a {
86 width: 100px;
87 }
88 }
89
90 @media screen and (min-width: 1000px) {
91 #peertube-title a {
92 width: 120px;
93 }
94 }
95
96 @media screen and (min-width: 1000px) {
97 #peertube-title a {
98 width: 120px;
99 }
100 }
101
102 @media screen and (min-width: 1200px) {
103 padding-left: 15px;
104
105 .hamburger-block {
106 margin-right: 15px;
107 }
108
109 #peertube-title a {
110 width: 135px;
111 }
112 }
113
114 @media screen and (min-width: 1600px) {
115 .hamburger-block {
116 margin-right: 20px;
117 }
118
119 #peertube-title a {
120 width: 180px;
121 }
122 } 69 }
123 } 70 }
124 71
125 my-search { 72 .header-right {
126 position: fixed; 73 height: $header-height;
127 z-index: 1000; 74 display: flex;
128 // Fix col-md-* padding 75 align-items: center;
129 padding: 0; 76 flex-grow: 1;
130 } 77 justify-content: flex-end;
131
132 .search-col {
133 height: 100%;
134 margin-left: -15px;
135 padding: 0;
136 } 78 }
137} 79}
138 80
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts
index 9b699fafd..b1818c298 100644
--- a/client/src/app/app.component.ts
+++ b/client/src/app/app.component.ts
@@ -1,8 +1,6 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3
4import { AuthService, ServerService } from './core' 3import { AuthService, ServerService } from './core'
5import { UserService } from './shared'
6 4
7@Component({ 5@Component({
8 selector: 'my-app', 6 selector: 'my-app',
@@ -62,20 +60,9 @@ export class AppComponent implements OnInit {
62 } 60 }
63 61
64 getMainColClasses () { 62 getMainColClasses () {
65 const colSizes = {
66 md: 10,
67 sm: 9,
68 xs: 9
69 }
70
71 // Take all width is the menu is not displayed 63 // Take all width is the menu is not displayed
72 if (this.isMenuDisplayed === false) { 64 if (this.isMenuDisplayed === false) return [ 'expanded' ]
73 Object.keys(colSizes).forEach(col => colSizes[col] = 12)
74 }
75
76 const classes = []
77 Object.keys(colSizes).forEach(col => classes.push(`col-${col}-${colSizes[col]}`))
78 65
79 return classes 66 return []
80 } 67 }
81} 68}
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts
index e71641e0d..1326e3411 100644
--- a/client/src/app/app.module.ts
+++ b/client/src/app/app.module.ts
@@ -20,6 +20,8 @@ import { LoginModule } from './login'
20import { SignupModule } from './signup' 20import { SignupModule } from './signup'
21import { SharedModule } from './shared' 21import { SharedModule } from './shared'
22import { VideosModule } from './videos' 22import { VideosModule } from './videos'
23import { MenuComponent } from './menu'
24import { HeaderComponent } from './header'
23 25
24export function metaFactory (): MetaLoader { 26export function metaFactory (): MetaLoader {
25 return new MetaStaticLoader({ 27 return new MetaStaticLoader({
@@ -47,7 +49,10 @@ const APP_PROVIDERS = [
47@NgModule({ 49@NgModule({
48 bootstrap: [ AppComponent ], 50 bootstrap: [ AppComponent ],
49 declarations: [ 51 declarations: [
50 AppComponent 52 AppComponent,
53
54 MenuComponent,
55 HeaderComponent
51 ], 56 ],
52 imports: [ 57 imports: [
53 BrowserModule, 58 BrowserModule,
diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts
index 9e6c6b888..e887dde1f 100644
--- a/client/src/app/core/auth/auth.service.ts
+++ b/client/src/app/core/auth/auth.service.ts
@@ -1,30 +1,25 @@
1import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'
1import { Injectable } from '@angular/core' 2import { Injectable } from '@angular/core'
2import { Router } from '@angular/router' 3import { Router } from '@angular/router'
3import { Observable } from 'rxjs/Observable' 4
4import { Subject } from 'rxjs/Subject' 5import { NotificationsService } from 'angular2-notifications'
5import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http' 6import 'rxjs/add/observable/throw'
6import { ReplaySubject } from 'rxjs/ReplaySubject'
7import 'rxjs/add/operator/do' 7import 'rxjs/add/operator/do'
8import 'rxjs/add/operator/map' 8import 'rxjs/add/operator/map'
9import 'rxjs/add/operator/mergeMap' 9import 'rxjs/add/operator/mergeMap'
10import 'rxjs/add/observable/throw' 10import { Observable } from 'rxjs/Observable'
11 11import { ReplaySubject } from 'rxjs/ReplaySubject'
12import { NotificationsService } from 'angular2-notifications' 12import { Subject } from 'rxjs/Subject'
13 13import { OAuthClientLocal, User as UserServerModel, UserRefreshToken, UserRole, VideoChannel } from '../../../../../shared'
14import { AuthStatus } from './auth-status.model' 14import { Account } from '../../../../../shared/models/accounts'
15import { AuthUser } from './auth-user.model' 15import { UserLogin } from '../../../../../shared/models/users/user-login.model'
16import {
17 OAuthClientLocal,
18 UserRole,
19 UserRefreshToken,
20 VideoChannel,
21 User as UserServerModel
22} from '../../../../../shared'
23// Do not use the barrel (dependency loop) 16// Do not use the barrel (dependency loop)
24import { RestExtractor } from '../../shared/rest' 17import { RestExtractor } from '../../shared/rest'
25import { UserLogin } from '../../../../../shared/models/users/user-login.model'
26import { UserConstructorHash } from '../../shared/users/user.model' 18import { UserConstructorHash } from '../../shared/users/user.model'
27 19
20import { AuthStatus } from './auth-status.model'
21import { AuthUser } from './auth-user.model'
22
28interface UserLoginWithUsername extends UserLogin { 23interface UserLoginWithUsername extends UserLogin {
29 access_token: string 24 access_token: string
30 refresh_token: string 25 refresh_token: string
@@ -42,10 +37,7 @@ interface UserLoginWithUserInformation extends UserLogin {
42 displayNSFW: boolean 37 displayNSFW: boolean
43 email: string 38 email: string
44 videoQuota: number 39 videoQuota: number
45 account: { 40 account: Account
46 id: number
47 uuid: string
48 }
49 videoChannels: VideoChannel[] 41 videoChannels: VideoChannel[]
50} 42}
51 43
@@ -177,19 +169,15 @@ export class AuthService {
177 169
178 return this.http.post<UserRefreshToken>(AuthService.BASE_TOKEN_URL, body, { headers }) 170 return this.http.post<UserRefreshToken>(AuthService.BASE_TOKEN_URL, body, { headers })
179 .map(res => this.handleRefreshToken(res)) 171 .map(res => this.handleRefreshToken(res))
180 .catch(res => { 172 .catch(err => {
181 // The refresh token is invalid? 173 console.error(err)
182 if (res.status === 400 && res.error.error === 'invalid_grant') { 174 console.log('Cannot refresh token -> logout...')
183 console.error('Cannot refresh token -> logout...') 175 this.logout()
184 this.logout() 176 this.router.navigate(['/login'])
185 this.router.navigate(['/login']) 177
186 178 return Observable.throw({
187 return Observable.throw({ 179 error: 'You need to reconnect.'
188 error: 'You need to reconnect.' 180 })
189 })
190 }
191
192 return this.restExtractor.handleError(res)
193 }) 181 })
194 } 182 }
195 183
@@ -202,7 +190,6 @@ export class AuthService {
202 } 190 }
203 191
204 this.mergeUserInformation(obj) 192 this.mergeUserInformation(obj)
205 .do(() => this.userInformationLoaded.next(true))
206 .subscribe( 193 .subscribe(
207 res => { 194 res => {
208 this.user.displayNSFW = res.displayNSFW 195 this.user.displayNSFW = res.displayNSFW
@@ -211,6 +198,8 @@ export class AuthService {
211 this.user.account = res.account 198 this.user.account = res.account
212 199
213 this.user.save() 200 this.user.save()
201
202 this.userInformationLoaded.next(true)
214 } 203 }
215 ) 204 )
216 } 205 }
diff --git a/client/src/app/core/confirm/confirm.component.html b/client/src/app/core/confirm/confirm.component.html
index 2726af6cc..31b735f97 100644
--- a/client/src/app/core/confirm/confirm.component.html
+++ b/client/src/app/core/confirm/confirm.component.html
@@ -6,14 +6,14 @@
6 <button type="button" class="close" aria-label="Close" (click)="cancel()"> 6 <button type="button" class="close" aria-label="Close" (click)="cancel()">
7 <span aria-hidden="true">&times;</span> 7 <span aria-hidden="true">&times;</span>
8 </button> 8 </button>
9 <h4 class="modal-title">{{ title }}</h4> 9 <h4 class="title-page title-page-single">{{ title }}</h4>
10 </div> 10 </div>
11 11
12 <div class="modal-body" [innerHtml]="message"></div> 12 <div class="modal-body" [innerHtml]="message"></div>
13 13
14 <div class="modal-footer"> 14 <div class="modal-footer">
15 <button type="button" class="btn btn-default" data-dismiss="modal" (click)="cancel()">Cancel</button> 15 <button type="button" class="grey-button" data-dismiss="modal" (click)="cancel()">Cancel</button>
16 <button type="button" class="btn btn-primary" (click)="confirm()">Confirm</button> 16 <button type="button" class="orange-button" (click)="confirm()">Confirm</button>
17 </div> 17 </div>
18 </div> 18 </div>
19 </div> 19 </div>
diff --git a/client/src/app/core/confirm/confirm.component.ts b/client/src/app/core/confirm/confirm.component.ts
index c8e41e233..0515d969a 100644
--- a/client/src/app/core/confirm/confirm.component.ts
+++ b/client/src/app/core/confirm/confirm.component.ts
@@ -11,7 +11,8 @@ export interface ConfigChangedEvent {
11 11
12@Component({ 12@Component({
13 selector: 'my-confirm', 13 selector: 'my-confirm',
14 templateUrl: './confirm.component.html' 14 templateUrl: './confirm.component.html',
15 styles: [ '.button { padding: 0 13px; }' ]
15}) 16})
16export class ConfirmComponent implements OnInit { 17export class ConfirmComponent implements OnInit {
17 @ViewChild('confirmModal') confirmModal: ModalDirective 18 @ViewChild('confirmModal') confirmModal: ModalDirective
diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts
index c4ce2b637..75262e6cf 100644
--- a/client/src/app/core/core.module.ts
+++ b/client/src/app/core/core.module.ts
@@ -26,17 +26,13 @@ import { throwIfAlreadyLoaded } from './module-import-guard'
26 ], 26 ],
27 27
28 declarations: [ 28 declarations: [
29 ConfirmComponent, 29 ConfirmComponent
30 MenuComponent,
31 MenuAdminComponent
32 ], 30 ],
33 31
34 exports: [ 32 exports: [
35 SimpleNotificationsModule, 33 SimpleNotificationsModule,
36 34
37 ConfirmComponent, 35 ConfirmComponent
38 MenuComponent,
39 MenuAdminComponent
40 ], 36 ],
41 37
42 providers: [ 38 providers: [
diff --git a/client/src/app/core/index.ts b/client/src/app/core/index.ts
index 8358261ae..3c01e05aa 100644
--- a/client/src/app/core/index.ts
+++ b/client/src/app/core/index.ts
@@ -1,6 +1,5 @@
1export * from './auth' 1export * from './auth'
2export * from './server' 2export * from './server'
3export * from './confirm' 3export * from './confirm'
4export * from './menu'
5export * from './routing' 4export * from './routing'
6export * from './core.module' 5export * from './core.module'
diff --git a/client/src/app/core/menu/index.ts b/client/src/app/core/menu/index.ts
deleted file mode 100644
index c905ed20a..000000000
--- a/client/src/app/core/menu/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
1export * from './menu.component'
2export * from './menu-admin.component'
diff --git a/client/src/app/core/menu/menu-admin.component.html b/client/src/app/core/menu/menu-admin.component.html
deleted file mode 100644
index 9857b2e3e..000000000
--- a/client/src/app/core/menu/menu-admin.component.html
+++ /dev/null
@@ -1,35 +0,0 @@
1<menu>
2 <div class="panel-block">
3 <a *ngIf="hasUsersRight()" routerLink="/admin/users" routerLinkActive="active">
4 <span class="hidden-xs glyphicon glyphicon-user"></span>
5 List users
6 </a>
7
8 <a *ngIf="hasServerFollowRight()" routerLink="/admin/follows" routerLinkActive="active">
9 <span class="hidden-xs glyphicon glyphicon-cloud"></span>
10 Manage follows
11 </a>
12
13 <a *ngIf="hasVideoAbusesRight()" routerLink="/admin/video-abuses" routerLinkActive="active">
14 <span class="hidden-xs glyphicon glyphicon-alert"></span>
15 Video abuses
16 </a>
17
18 <a *ngIf="hasVideoBlacklistRight()" routerLink="/admin/video-blacklist" routerLinkActive="active">
19 <span class="hidden-xs glyphicon glyphicon-eye-close"></span>
20 Video blacklist
21 </a>
22
23 <a *ngIf="hasJobsRight()" routerLink="/admin/jobs" routerLinkActive="active">
24 <span class="hidden-xs glyphicon glyphicon-tasks"></span>
25 Jobs
26 </a>
27 </div>
28
29 <div class="panel-block">
30 <a routerLink="/videos/list" routerLinkActive="active">
31 <span class="hidden-xs glyphicon glyphicon-cog"></span>
32 Quit admin.
33 </a>
34 </div>
35</menu>
diff --git a/client/src/app/core/menu/menu-admin.component.ts b/client/src/app/core/menu/menu-admin.component.ts
deleted file mode 100644
index ea8d5f57c..000000000
--- a/client/src/app/core/menu/menu-admin.component.ts
+++ /dev/null
@@ -1,33 +0,0 @@
1import { Component } from '@angular/core'
2
3import { AuthService } from '../auth/auth.service'
4import { UserRight } from '../../../../../shared'
5
6@Component({
7 selector: 'my-menu-admin',
8 templateUrl: './menu-admin.component.html',
9 styleUrls: [ './menu.component.scss' ]
10})
11export class MenuAdminComponent {
12 constructor (private auth: AuthService) {}
13
14 hasUsersRight () {
15 return this.auth.getUser().hasRight(UserRight.MANAGE_USERS)
16 }
17
18 hasServerFollowRight () {
19 return this.auth.getUser().hasRight(UserRight.MANAGE_SERVER_FOLLOW)
20 }
21
22 hasVideoAbusesRight () {
23 return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_ABUSES)
24 }
25
26 hasVideoBlacklistRight () {
27 return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
28 }
29
30 hasJobsRight () {
31 return this.auth.getUser().hasRight(UserRight.MANAGE_JOBS)
32 }
33}
diff --git a/client/src/app/core/menu/menu.component.html b/client/src/app/core/menu/menu.component.html
deleted file mode 100644
index fcde23fdd..000000000
--- a/client/src/app/core/menu/menu.component.html
+++ /dev/null
@@ -1,55 +0,0 @@
1<menu>
2 <div class="panel-block">
3 <div class="block-title">Account</div>
4
5 <div id="panel-user-login" class="panel-button">
6 <a *ngIf="!isLoggedIn" routerLink="/login" routerLinkActive="active">
7 <span class="hidden-xs glyphicon glyphicon-log-in"></span>
8 Login
9 </a>
10
11 <a *ngIf="isLoggedIn" (click)="logout()">
12 <span class="hidden-xs glyphicon glyphicon-log-out"></span>
13 Logout
14 </a>
15 </div>
16
17 <a *ngIf="!isLoggedIn && isRegistrationAllowed()" routerLink="/signup" routerLinkActive="active">
18 <span class="hidden-xs glyphicon glyphicon-user"></span>
19 Signup
20 </a>
21
22 <a *ngIf="isLoggedIn" routerLink="/account" routerLinkActive="active">
23 <span class="hidden-xs glyphicon glyphicon-user"></span>
24 My account
25 </a>
26
27 <a *ngIf="isLoggedIn" routerLink="/videos/mine" routerLinkActive="active">
28 <span class="hidden-xs glyphicon glyphicon-folder-open"></span>
29 My videos
30 </a>
31 </div>
32
33 <div class="panel-block">
34 <div class="block-title">Videos</div>
35
36 <a routerLink="/videos/list" routerLinkActive="active">
37 <span class="hidden-xs glyphicon glyphicon-list"></span>
38 See videos
39 </a>
40
41 <a *ngIf="isLoggedIn" routerLink="/videos/upload" routerLinkActive="active">
42 <span class="hidden-xs glyphicon glyphicon-cloud-upload"></span>
43 Upload a video
44 </a>
45 </div>
46
47 <div *ngIf="userHasAdminAccess" class="panel-block">
48 <div class="block-title">Other</div>
49
50 <a [routerLink]="getFirstAdminRouteAvailable()" routerLinkActive="active">
51 <span class="hidden-xs glyphicon glyphicon-cog"></span>
52 Administration
53 </a>
54 </div>
55</menu>
diff --git a/client/src/app/core/menu/menu.component.scss b/client/src/app/core/menu/menu.component.scss
deleted file mode 100644
index 45679c310..000000000
--- a/client/src/app/core/menu/menu.component.scss
+++ /dev/null
@@ -1,51 +0,0 @@
1menu {
2 background-color: $black-background;
3 padding: 15px;
4 margin: 0;
5 height: 100%;
6 white-space: nowrap;
7 text-overflow: ellipsis;
8 overflow: hidden;
9 z-index: 1000;
10
11 @media screen and (max-width: 550px) {
12 font-size: 90%;
13 }
14
15 @media screen and (min-width: 1200px) {
16 padding: 25px;
17 }
18
19 .panel-block {
20 margin-bottom: 15px;
21 }
22
23 .block-title {
24 text-transform: uppercase;
25 font-weight: bold;
26 color: $menu-color-block;
27 margin-bottom: 10px;
28 }
29
30 a {
31 display: block;
32 margin-left: 5px;
33 height: 30px;
34 color: $menu-color-link;
35 cursor: pointer;
36 transition: color 0.3s;
37
38 &:hover, &:focus {
39 text-decoration: none !important;
40 outline: none !important;
41 }
42
43 .glyphicon {
44 margin-right: 15px;
45 }
46
47 &:hover, &.active {
48 color: #fff;
49 }
50 }
51}
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index cbc4074c9..16e0595b6 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -1,5 +1,7 @@
1import { Injectable } from '@angular/core'
2import { HttpClient } from '@angular/common/http' 1import { HttpClient } from '@angular/common/http'
2import { Injectable } from '@angular/core'
3import 'rxjs/add/operator/do'
4import { ReplaySubject } from 'rxjs/ReplaySubject'
3 5
4import { ServerConfig } from '../../../../../shared' 6import { ServerConfig } from '../../../../../shared'
5 7
@@ -8,6 +10,11 @@ export class ServerService {
8 private static BASE_CONFIG_URL = API_URL + '/api/v1/config/' 10 private static BASE_CONFIG_URL = API_URL + '/api/v1/config/'
9 private static BASE_VIDEO_URL = API_URL + '/api/v1/videos/' 11 private static BASE_VIDEO_URL = API_URL + '/api/v1/videos/'
10 12
13 videoPrivaciesLoaded = new ReplaySubject<boolean>(1)
14 videoCategoriesLoaded = new ReplaySubject<boolean>(1)
15 videoLicencesLoaded = new ReplaySubject<boolean>(1)
16 videoLanguagesLoaded = new ReplaySubject<boolean>(1)
17
11 private config: ServerConfig = { 18 private config: ServerConfig = {
12 signup: { 19 signup: {
13 allowed: false 20 allowed: false
@@ -29,19 +36,19 @@ export class ServerService {
29 } 36 }
30 37
31 loadVideoCategories () { 38 loadVideoCategories () {
32 return this.loadVideoAttributeEnum('categories', this.videoCategories) 39 return this.loadVideoAttributeEnum('categories', this.videoCategories, this.videoCategoriesLoaded)
33 } 40 }
34 41
35 loadVideoLicences () { 42 loadVideoLicences () {
36 return this.loadVideoAttributeEnum('licences', this.videoLicences) 43 return this.loadVideoAttributeEnum('licences', this.videoLicences, this.videoLicencesLoaded)
37 } 44 }
38 45
39 loadVideoLanguages () { 46 loadVideoLanguages () {
40 return this.loadVideoAttributeEnum('languages', this.videoLanguages) 47 return this.loadVideoAttributeEnum('languages', this.videoLanguages, this.videoLanguagesLoaded)
41 } 48 }
42 49
43 loadVideoPrivacies () { 50 loadVideoPrivacies () {
44 return this.loadVideoAttributeEnum('privacies', this.videoPrivacies) 51 return this.loadVideoAttributeEnum('privacies', this.videoPrivacies, this.videoPrivaciesLoaded)
45 } 52 }
46 53
47 getConfig () { 54 getConfig () {
@@ -66,17 +73,20 @@ export class ServerService {
66 73
67 private loadVideoAttributeEnum ( 74 private loadVideoAttributeEnum (
68 attributeName: 'categories' | 'licences' | 'languages' | 'privacies', 75 attributeName: 'categories' | 'licences' | 'languages' | 'privacies',
69 hashToPopulate: { id: number, label: string }[] 76 hashToPopulate: { id: number, label: string }[],
77 notifier: ReplaySubject<boolean>
70 ) { 78 ) {
71 return this.http.get(ServerService.BASE_VIDEO_URL + attributeName) 79 return this.http.get(ServerService.BASE_VIDEO_URL + attributeName)
72 .subscribe(data => { 80 .subscribe(data => {
73 Object.keys(data) 81 Object.keys(data)
74 .forEach(dataKey => { 82 .forEach(dataKey => {
75 hashToPopulate.push({ 83 hashToPopulate.push({
76 id: parseInt(dataKey, 10), 84 id: parseInt(dataKey, 10),
77 label: data[dataKey] 85 label: data[dataKey]
78 }) 86 })
79 })
80 }) 87 })
88
89 notifier.next(true)
90 })
81 } 91 }
82} 92}
diff --git a/client/src/app/header/header.component.html b/client/src/app/header/header.component.html
new file mode 100644
index 000000000..c853d2b1b
--- /dev/null
+++ b/client/src/app/header/header.component.html
@@ -0,0 +1,10 @@
1<input
2 type="text" id="search-video" name="search-video" placeholder="Search..."
3 [(ngModel)]="searchValue" (keyup.enter)="doSearch()"
4>
5<span (click)="doSearch()" class="icon icon-search"></span>
6
7<a class="upload-button" routerLink="/videos/upload">
8 <span class="icon icon-upload"></span>
9 <span class="upload-button-label">Upload</span>
10</a>
diff --git a/client/src/app/header/header.component.scss b/client/src/app/header/header.component.scss
new file mode 100644
index 000000000..fba70dd2f
--- /dev/null
+++ b/client/src/app/header/header.component.scss
@@ -0,0 +1,58 @@
1#search-video {
2 @include peertube-input-text($search-input-width);
3 margin-right: 15px;
4 padding-right: 25px; // For the search icon
5
6 &::placeholder {
7 color: #000;
8 }
9
10 @media screen and (max-width: 600px) {
11 width: calc(100% - 150px);
12 }
13
14 @media screen and (max-width: 400px) {
15 width: calc(100% - 70px);
16 }
17}
18
19.icon.icon-search {
20 @include icon(25px);
21 height: 21px;
22
23 background-image: url('../../assets/images/header/search.svg');
24
25 // yolo
26 position: absolute;
27 margin-left: -50px;
28 margin-top: 5px;
29}
30
31.upload-button {
32 @include peertube-button-link;
33 @include orange-button;
34
35 margin-right: 25px;
36
37 .icon.icon-upload {
38 @include icon(22px);
39
40 background-image: url('../../assets/images/header/upload.svg');
41 height: 24px;
42 vertical-align: middle;
43 margin-right: 6px;
44 }
45
46 @media screen and (max-width: 400px) {
47 margin-right: 10px;
48 padding: 0 10px;
49
50 .icon.icon-upload {
51 margin-right: 0;
52 }
53
54 .upload-button-label {
55 display: none;
56 }
57 }
58}
diff --git a/client/src/app/header/header.component.ts b/client/src/app/header/header.component.ts
new file mode 100644
index 000000000..a903048f2
--- /dev/null
+++ b/client/src/app/header/header.component.ts
@@ -0,0 +1,28 @@
1import { Component, OnInit } from '@angular/core'
2import { Router } from '@angular/router'
3import { getParameterByName } from '../shared/misc/utils'
4
5@Component({
6 selector: 'my-header',
7 templateUrl: './header.component.html',
8 styleUrls: [ './header.component.scss' ]
9})
10
11export class HeaderComponent implements OnInit {
12 searchValue = ''
13
14 constructor (private router: Router) {}
15
16 ngOnInit () {
17 const searchQuery = getParameterByName('search', window.location.href)
18 if (searchQuery) this.searchValue = searchQuery
19 }
20
21 doSearch () {
22 if (!this.searchValue) return
23
24 this.router.navigate([ '/videos', 'search' ], {
25 queryParams: { search: this.searchValue }
26 })
27 }
28}
diff --git a/client/src/app/header/index.ts b/client/src/app/header/index.ts
new file mode 100644
index 000000000..d98d2d00a
--- /dev/null
+++ b/client/src/app/header/index.ts
@@ -0,0 +1 @@
export * from './header.component'
diff --git a/client/src/app/login/login.component.html b/client/src/app/login/login.component.html
index bcea0a27a..24807987c 100644
--- a/client/src/app/login/login.component.html
+++ b/client/src/app/login/login.component.html
@@ -1,34 +1,33 @@
1<div class="row"> 1<div class="margin-content">
2 <div class="content-padding"> 2 <div class="title-page title-page-single">
3 3 Login
4 <h3>Login</h3> 4 </div>
5 5
6 <div *ngIf="error" class="alert alert-danger">{{ error }}</div> 6 <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
7 7
8 <form role="form" (ngSubmit)="login()" [formGroup]="form"> 8 <form role="form" (ngSubmit)="login()" [formGroup]="form">
9 <div class="form-group"> 9 <div class="form-group">
10 <label for="username">Username</label> 10 <label for="username">Username</label>
11 <input 11 <input
12 type="text" class="form-control" id="username" placeholder="Username" required 12 type="text" id="username" placeholder="Username" required
13 formControlName="username" 13 formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }"
14 > 14 >
15 <div *ngIf="formErrors.username" class="alert alert-danger"> 15 <div *ngIf="formErrors.username" class="form-error">
16 {{ formErrors.username }} 16 {{ formErrors.username }}
17 </div>
18 </div> 17 </div>
18 </div>
19 19
20 <div class="form-group"> 20 <div class="form-group">
21 <label for="password">Password</label> 21 <label for="password">Password</label>
22 <input 22 <input
23 type="password" class="form-control" name="password" id="password" placeholder="Password" required 23 type="password" name="password" id="password" placeholder="Password" required
24 formControlName="password" 24 formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
25 > 25 >
26 <div *ngIf="formErrors.password" class="alert alert-danger"> 26 <div *ngIf="formErrors.password" class="form-error">
27 {{ formErrors.password }} 27 {{ formErrors.password }}
28 </div>
29 </div> 28 </div>
29 </div>
30 30
31 <input type="submit" value="Login" class="btn btn-default" [disabled]="!form.valid"> 31 <input type="submit" value="Login" [disabled]="!form.valid">
32 </form> 32 </form>
33 </div>
34</div> 33</div>
diff --git a/client/src/app/login/login.component.scss b/client/src/app/login/login.component.scss
new file mode 100644
index 000000000..3b4326de4
--- /dev/null
+++ b/client/src/app/login/login.component.scss
@@ -0,0 +1,9 @@
1input:not([type=submit]) {
2 @include peertube-input-text(340px);
3 display: block;
4}
5
6input[type=submit] {
7 @include peertube-button;
8 @include orange-button;
9}
diff --git a/client/src/app/login/login.component.ts b/client/src/app/login/login.component.ts
index 32dc9e36f..dfede5924 100644
--- a/client/src/app/login/login.component.ts
+++ b/client/src/app/login/login.component.ts
@@ -7,7 +7,8 @@ import { FormReactive } from '../shared'
7 7
8@Component({ 8@Component({
9 selector: 'my-login', 9 selector: 'my-login',
10 templateUrl: './login.component.html' 10 templateUrl: './login.component.html',
11 styleUrls: [ './login.component.scss' ]
11}) 12})
12 13
13export class LoginComponent extends FormReactive implements OnInit { 14export class LoginComponent extends FormReactive implements OnInit {
diff --git a/client/src/app/menu/index.ts b/client/src/app/menu/index.ts
new file mode 100644
index 000000000..421271c12
--- /dev/null
+++ b/client/src/app/menu/index.ts
@@ -0,0 +1 @@
export * from './menu.component'
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html
new file mode 100644
index 000000000..7a80fa4de
--- /dev/null
+++ b/client/src/app/menu/menu.component.html
@@ -0,0 +1,50 @@
1<menu>
2 <div *ngIf="isLoggedIn" class="logged-in-block">
3 <img [src]="getUserAvatarPath()" alt="Avatar" />
4
5 <div class="logged-in-info">
6 <a routerLink="/account/settings" class="logged-in-username">{{ user.username }}</a>
7 <div class="logged-in-email">{{ user.email }}</div>
8 </div>
9
10 <div class="logged-in-more" dropdown placement="right" container="body">
11 <span class="glyphicon glyphicon-option-vertical" dropdownToggle></span>
12
13 <ul *dropdownMenu class="dropdown-menu">
14 <li>
15 <a (click)="logout($event)" class="dropdown-item" title="Log out" href="#">
16 Log out
17 </a>
18 </li>
19 </ul>
20 </div>
21 </div>
22
23 <div *ngIf="!isLoggedIn" class="button-block">
24 <a routerLink="/login" class="login-button">Login</a>
25 <a *ngIf="isRegistrationAllowed()" routerLink="/signup" class="create-account-button">Create an account</a>
26 </div>
27
28 <div class="panel-block">
29 <div class="block-title">Videos</div>
30
31 <a routerLink="/videos/trending" routerLinkActive="active">
32 <span class="icon icon-videos-trending"></span>
33 Trending
34 </a>
35
36 <a routerLink="/videos/recently-added" routerLinkActive="active">
37 <span class="icon icon-videos-recently-added"></span>
38 Recently added
39 </a>
40 </div>
41
42 <div *ngIf="userHasAdminAccess" class="panel-block">
43 <div class="block-title">More</div>
44
45 <a [routerLink]="getFirstAdminRouteAvailable()" routerLinkActive="active">
46 <span class="icon icon-administration"></span>
47 Administration
48 </a>
49 </div>
50</menu>
diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss
new file mode 100644
index 000000000..97ceadde3
--- /dev/null
+++ b/client/src/app/menu/menu.component.scss
@@ -0,0 +1,193 @@
1menu {
2 background-color: $black-background;
3 margin: 0;
4 padding: 0;
5 height: 100%;
6 white-space: nowrap;
7 text-overflow: ellipsis;
8 overflow: hidden;
9 z-index: 1000;
10 color: $menu-color;
11
12 .logged-in-block {
13 height: 100px;
14 background-color: rgba(255, 255, 255, 0.15);
15 display: flex;
16 align-items: center;
17 justify-content: center;
18 margin-bottom: 35px;
19
20 img {
21 margin-left: 20px;
22 margin-right: 10px;
23
24 @include avatar(34px);
25 }
26
27 .logged-in-info {
28 flex-grow: 1;
29
30 .logged-in-username {
31 font-size: 16px;
32 font-weight: $font-semibold;
33 color: $menu-color;
34 cursor: pointer;
35
36 @include disable-default-a-behaviour;
37 }
38
39 .logged-in-email {
40 font-size: 13px;
41 color: #C6C6C6;
42 white-space: nowrap;
43 overflow: hidden;
44 text-overflow: ellipsis;
45 max-width: 140px;
46 }
47 }
48
49 .logged-in-more {
50 margin-right: 20px;
51
52 .glyphicon {
53 cursor: pointer;
54 font-size: 18px;
55 }
56 }
57 }
58
59 .button-block {
60 margin: 30px 25px 35px 25px;
61
62 .login-button, .create-account-button {
63 font-weight: $font-semibold;
64 font-size: 15px;
65 height: $button-height;
66 line-height: $button-height;
67 width: 100%;
68 border-radius: 3px;
69 text-align: center;
70 color: $menu-color;
71 display: block;
72 cursor: pointer;
73 margin-bottom: 15px;
74
75 @include disable-default-a-behaviour;
76
77 &.login-button {
78 background-color: $orange-color;
79 margin-bottom: 10px;
80 }
81
82 &.create-account-button {
83 background-color: rgba(255, 255, 255, 0.25);
84 }
85 }
86 }
87
88 .block-title {
89 text-transform: uppercase;
90 font-weight: $font-bold; // Bold
91 font-size: 13px;
92 margin-bottom: 25px;
93 }
94
95 .panel-block {
96 margin-bottom: 45px;
97 margin-left: 26px;
98
99 a {
100 display: flex;
101 color: $menu-color;
102 cursor: pointer;
103 height: 22px;
104 line-height: 22px;
105 font-size: 16px;
106 margin-bottom: 15px;
107 @include disable-default-a-behaviour;
108
109 .icon {
110 @include icon(22px);
111
112 margin-right: 18px;
113
114 &.icon-videos-trending {
115 position: relative;
116 top: -2px;
117 background-image: url('../../assets/images/menu/trending.svg');
118 }
119
120 &.icon-videos-recently-added {
121 width: 23px;
122 height: 23px;
123 position: relative;
124 top: -1px;
125 background-image: url('../../assets/images/menu/recently-added.svg');
126 }
127
128 &.icon-administration {
129 width: 23px;
130 height: 23px;
131
132 background-image: url('../../assets/images/menu/administration.svg');
133 }
134 }
135 }
136 }
137}
138
139@media screen and (max-width: 800px) {
140 menu {
141 .logged-in-block {
142 padding-left: 10px;
143
144 img {
145 display: none;
146 }
147
148 .logged-in-info {
149 .logged-in-username {
150 font-size: 14px;
151 }
152
153 .logged-in-email {
154 font-size: 11px;
155 max-width: 120px;
156 }
157 }
158
159 .logged-in-more {
160 margin-right: 5px;
161
162 .login-button, .create-account-button {
163 font-weight: $font-semibold;
164 font-size: 15px;
165 height: $button-height;
166 line-height: $button-height;
167 width: 190px;
168 }
169 }
170 }
171
172 .button-block {
173 margin: 20px 10px 25px 10px;
174
175 .login-button, .create-account-button {
176 font-size: 13px;
177 }
178 }
179
180 .panel-block {
181 margin-bottom: 30px;
182 margin-left: 10px;
183
184 a {
185 font-size: 14px;
186
187 .icon {
188 margin-right: 10px;
189 }
190 }
191 }
192 }
193}
diff --git a/client/src/app/core/menu/menu.component.ts b/client/src/app/menu/menu.component.ts
index d2bd71534..8b8b714a8 100644
--- a/client/src/app/core/menu/menu.component.ts
+++ b/client/src/app/menu/menu.component.ts
@@ -1,9 +1,8 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3 3import { UserRight } from '../../../../shared/models/users/user-right.enum'
4import { AuthService, AuthStatus } from '../auth' 4import { AuthService, AuthStatus, ServerService } from '../core'
5import { ServerService } from '../server' 5import { User } from '../shared/users/user.model'
6import { UserRight } from '../../../../../shared/models/users/user-right.enum'
7 6
8@Component({ 7@Component({
9 selector: 'my-menu', 8 selector: 'my-menu',
@@ -11,6 +10,7 @@ import { UserRight } from '../../../../../shared/models/users/user-right.enum'
11 styleUrls: [ './menu.component.scss' ] 10 styleUrls: [ './menu.component.scss' ]
12}) 11})
13export class MenuComponent implements OnInit { 12export class MenuComponent implements OnInit {
13 user: User
14 isLoggedIn: boolean 14 isLoggedIn: boolean
15 userHasAdminAccess = false 15 userHasAdminAccess = false
16 16
@@ -29,16 +29,19 @@ export class MenuComponent implements OnInit {
29 29
30 ngOnInit () { 30 ngOnInit () {
31 this.isLoggedIn = this.authService.isLoggedIn() 31 this.isLoggedIn = this.authService.isLoggedIn()
32 if (this.isLoggedIn === true) this.user = this.authService.getUser()
32 this.computeIsUserHasAdminAccess() 33 this.computeIsUserHasAdminAccess()
33 34
34 this.authService.loginChangedSource.subscribe( 35 this.authService.loginChangedSource.subscribe(
35 status => { 36 status => {
36 if (status === AuthStatus.LoggedIn) { 37 if (status === AuthStatus.LoggedIn) {
37 this.isLoggedIn = true 38 this.isLoggedIn = true
39 this.user = this.authService.getUser()
38 this.computeIsUserHasAdminAccess() 40 this.computeIsUserHasAdminAccess()
39 console.log('Logged in.') 41 console.log('Logged in.')
40 } else if (status === AuthStatus.LoggedOut) { 42 } else if (status === AuthStatus.LoggedOut) {
41 this.isLoggedIn = false 43 this.isLoggedIn = false
44 this.user = undefined
42 this.computeIsUserHasAdminAccess() 45 this.computeIsUserHasAdminAccess()
43 console.log('Logged out.') 46 console.log('Logged out.')
44 } else { 47 } else {
@@ -48,6 +51,10 @@ export class MenuComponent implements OnInit {
48 ) 51 )
49 } 52 }
50 53
54 getUserAvatarPath () {
55 return this.user.getAvatarPath()
56 }
57
51 isRegistrationAllowed () { 58 isRegistrationAllowed () {
52 return this.serverService.getConfig().signup.allowed 59 return this.serverService.getConfig().signup.allowed
53 } 60 }
@@ -78,7 +85,9 @@ export class MenuComponent implements OnInit {
78 return this.routesPerRight[right] 85 return this.routesPerRight[right]
79 } 86 }
80 87
81 logout () { 88 logout (event: Event) {
89 event.preventDefault()
90
82 this.authService.logout() 91 this.authService.logout()
83 // Redirect to home page 92 // Redirect to home page
84 this.router.navigate(['/videos/list']) 93 this.router.navigate(['/videos/list'])
diff --git a/client/src/app/shared/account/account.model.ts b/client/src/app/shared/account/account.model.ts
new file mode 100644
index 000000000..0b008188a
--- /dev/null
+++ b/client/src/app/shared/account/account.model.ts
@@ -0,0 +1,20 @@
1import { Account as ServerAccount } from '../../../../../shared/models/accounts/account.model'
2import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
3
4export class Account implements ServerAccount {
5 id: number
6 uuid: string
7 name: string
8 host: string
9 followingCount: number
10 followersCount: number
11 createdAt: Date
12 updatedAt: Date
13 avatar: Avatar
14
15 static GET_ACCOUNT_AVATAR_PATH (account: Account) {
16 if (account && account.avatar) return account.avatar.path
17
18 return API_URL + '/client/assets/images/default-avatar.png'
19 }
20}
diff --git a/client/src/app/shared/forms/form-validators/host.validator.ts b/client/src/app/shared/forms/form-validators/host.validator.ts
index 03e810fdb..c18a35f9b 100644
--- a/client/src/app/shared/forms/form-validators/host.validator.ts
+++ b/client/src/app/shared/forms/form-validators/host.validator.ts
@@ -1,14 +1,8 @@
1import { FormControl } from '@angular/forms' 1export function validateHost (value: string) {
2
3export function validateHost (c: FormControl) {
4 // Thanks to http://stackoverflow.com/a/106223 2 // Thanks to http://stackoverflow.com/a/106223
5 const HOST_REGEXP = new RegExp( 3 const HOST_REGEXP = new RegExp(
6 '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$' 4 '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$'
7 ) 5 )
8 6
9 return HOST_REGEXP.test(c.value) ? null : { 7 return HOST_REGEXP.test(value)
10 validateHost: {
11 valid: false
12 }
13 }
14} 8}
diff --git a/client/src/app/shared/forms/form-validators/video-abuse.ts b/client/src/app/shared/forms/form-validators/video-abuse.ts
index 3c7f26205..4b2a2b789 100644
--- a/client/src/app/shared/forms/form-validators/video-abuse.ts
+++ b/client/src/app/shared/forms/form-validators/video-abuse.ts
@@ -3,8 +3,8 @@ import { Validators } from '@angular/forms'
3export const VIDEO_ABUSE_REASON = { 3export const VIDEO_ABUSE_REASON = {
4 VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(300) ], 4 VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(300) ],
5 MESSAGES: { 5 MESSAGES: {
6 'required': 'Report reason name is required.', 6 'required': 'Report reason is required.',
7 'minlength': 'Report reson must be at least 2 characters long.', 7 'minlength': 'Report reason must be at least 2 characters long.',
8 'maxlength': 'Report reson cannot be more than 300 characters long.' 8 'maxlength': 'Report reason cannot be more than 300 characters long.'
9 } 9 }
10} 10}
diff --git a/client/src/app/shared/forms/form-validators/video.ts b/client/src/app/shared/forms/form-validators/video.ts
index 65f11f5da..45da7df4a 100644
--- a/client/src/app/shared/forms/form-validators/video.ts
+++ b/client/src/app/shared/forms/form-validators/video.ts
@@ -1,5 +1,11 @@
1import { Validators } from '@angular/forms' 1import { Validators } from '@angular/forms'
2 2
3export type ValidatorMessage = {
4 [ id: string ]: {
5 [ error: string ]: string
6 }
7}
8
3export const VIDEO_NAME = { 9export const VIDEO_NAME = {
4 VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(120) ], 10 VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(120) ],
5 MESSAGES: { 11 MESSAGES: {
@@ -17,17 +23,13 @@ export const VIDEO_PRIVACY = {
17} 23}
18 24
19export const VIDEO_CATEGORY = { 25export const VIDEO_CATEGORY = {
20 VALIDATORS: [ Validators.required ], 26 VALIDATORS: [ ],
21 MESSAGES: { 27 MESSAGES: {}
22 'required': 'Video category is required.'
23 }
24} 28}
25 29
26export const VIDEO_LICENCE = { 30export const VIDEO_LICENCE = {
27 VALIDATORS: [ Validators.required ], 31 VALIDATORS: [ ],
28 MESSAGES: { 32 MESSAGES: {}
29 'required': 'Video licence is required.'
30 }
31} 33}
32 34
33export const VIDEO_LANGUAGE = { 35export const VIDEO_LANGUAGE = {
@@ -43,9 +45,8 @@ export const VIDEO_CHANNEL = {
43} 45}
44 46
45export const VIDEO_DESCRIPTION = { 47export const VIDEO_DESCRIPTION = {
46 VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(3000) ], 48 VALIDATORS: [ Validators.minLength(3), Validators.maxLength(3000) ],
47 MESSAGES: { 49 MESSAGES: {
48 'required': 'Video description is required.',
49 'minlength': 'Video description must be at least 3 characters long.', 50 'minlength': 'Video description must be at least 3 characters long.',
50 'maxlength': 'Video description cannot be more than 3000 characters long.' 51 'maxlength': 'Video description cannot be more than 3000 characters long.'
51 } 52 }
@@ -58,10 +59,3 @@ export const VIDEO_TAGS = {
58 'maxlength': 'A tag should be less than 30 characters long.' 59 'maxlength': 'A tag should be less than 30 characters long.'
59 } 60 }
60} 61}
61
62export const VIDEO_FILE = {
63 VALIDATORS: [ Validators.required ],
64 MESSAGES: {
65 'required': 'Video file is required.'
66 }
67}
diff --git a/client/src/app/shared/index.ts b/client/src/app/shared/index.ts
index 79bf5ef43..413dda16a 100644
--- a/client/src/app/shared/index.ts
+++ b/client/src/app/shared/index.ts
@@ -1,7 +1,6 @@
1export * from './auth' 1export * from './auth'
2export * from './forms' 2export * from './forms'
3export * from './rest' 3export * from './rest'
4export * from './search'
5export * from './users' 4export * from './users'
6export * from './video-abuse' 5export * from './video-abuse'
7export * from './video-blacklist' 6export * from './video-blacklist'
diff --git a/client/src/app/shared/misc/button.component.scss b/client/src/app/shared/misc/button.component.scss
new file mode 100644
index 000000000..5fcae4f10
--- /dev/null
+++ b/client/src/app/shared/misc/button.component.scss
@@ -0,0 +1,27 @@
1.action-button {
2 @include peertube-button-link;
3
4 font-size: 15px;
5 font-weight: $font-semibold;
6 color: #585858;
7 background-color: #E5E5E5;
8
9 &:hover {
10 background-color: #EFEFEF;
11 }
12
13 .icon {
14 @include icon(21px);
15
16 position: relative;
17 top: -2px;
18
19 &.icon-edit {
20 background-image: url('../../../assets/images/global/edit.svg');
21 }
22
23 &.icon-delete-grey {
24 background-image: url('../../../assets/images/global/delete-grey.svg');
25 }
26 }
27}
diff --git a/client/src/app/shared/misc/delete-button.component.html b/client/src/app/shared/misc/delete-button.component.html
new file mode 100644
index 000000000..3db483882
--- /dev/null
+++ b/client/src/app/shared/misc/delete-button.component.html
@@ -0,0 +1,4 @@
1<span class="action-button action-button-delete" >
2 <span class="icon icon-delete-grey"></span>
3 Delete
4</span>
diff --git a/client/src/app/shared/misc/delete-button.component.ts b/client/src/app/shared/misc/delete-button.component.ts
new file mode 100644
index 000000000..e04039f69
--- /dev/null
+++ b/client/src/app/shared/misc/delete-button.component.ts
@@ -0,0 +1,10 @@
1import { Component } from '@angular/core'
2
3@Component({
4 selector: 'my-delete-button',
5 styleUrls: [ './button.component.scss' ],
6 templateUrl: './delete-button.component.html'
7})
8
9export class DeleteButtonComponent {
10}
diff --git a/client/src/app/shared/misc/edit-button.component.html b/client/src/app/shared/misc/edit-button.component.html
new file mode 100644
index 000000000..6e9564bd7
--- /dev/null
+++ b/client/src/app/shared/misc/edit-button.component.html
@@ -0,0 +1,4 @@
1<a class="action-button" [routerLink]="routerLink">
2 <span class="icon icon-edit"></span>
3 Edit
4</a>
diff --git a/client/src/app/shared/misc/edit-button.component.ts b/client/src/app/shared/misc/edit-button.component.ts
new file mode 100644
index 000000000..201a618ec
--- /dev/null
+++ b/client/src/app/shared/misc/edit-button.component.ts
@@ -0,0 +1,11 @@
1import { Component, Input } from '@angular/core'
2
3@Component({
4 selector: 'my-edit-button',
5 styleUrls: [ './button.component.scss' ],
6 templateUrl: './edit-button.component.html'
7})
8
9export class EditButtonComponent {
10 @Input() routerLink = []
11}
diff --git a/client/src/app/shared/misc/from-now.pipe.ts b/client/src/app/shared/misc/from-now.pipe.ts
new file mode 100644
index 000000000..fac02af0b
--- /dev/null
+++ b/client/src/app/shared/misc/from-now.pipe.ts
@@ -0,0 +1,36 @@
1import { Pipe, PipeTransform } from '@angular/core'
2
3// Thanks: https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site
4@Pipe({ name: 'myFromNow' })
5export class FromNowPipe implements PipeTransform {
6
7 transform (value: number) {
8 const seconds = Math.floor((Date.now() - value) / 1000)
9
10 let interval = Math.floor(seconds / 31536000)
11 if (interval > 1) {
12 return interval + ' years ago'
13 }
14
15 interval = Math.floor(seconds / 2592000)
16 if (interval > 1) return interval + ' months ago'
17 if (interval === 1) return interval + ' month ago'
18
19 interval = Math.floor(seconds / 604800)
20 if (interval > 1) return interval + ' weeks ago'
21 if (interval === 1) return interval + ' week ago'
22
23 interval = Math.floor(seconds / 86400)
24 if (interval > 1) return interval + ' days ago'
25 if (interval === 1) return interval + ' day ago'
26
27 interval = Math.floor(seconds / 3600)
28 if (interval > 1) return interval + ' hours ago'
29 if (interval === 1) return interval + ' hour ago'
30
31 interval = Math.floor(seconds / 60)
32 if (interval >= 1) return interval + ' min ago'
33
34 return Math.floor(seconds) + ' sec ago'
35 }
36}
diff --git a/client/src/app/shared/misc/number-formatter.pipe.ts b/client/src/app/shared/misc/number-formatter.pipe.ts
new file mode 100644
index 000000000..8a0756a36
--- /dev/null
+++ b/client/src/app/shared/misc/number-formatter.pipe.ts
@@ -0,0 +1,19 @@
1import { Pipe, PipeTransform } from '@angular/core'
2
3// Thanks: https://github.com/danrevah/ngx-pipes/blob/master/src/pipes/math/bytes.ts
4
5@Pipe({ name: 'myNumberFormatter' })
6export class NumberFormatterPipe implements PipeTransform {
7 private dictionary: Array<{max: number, type: string}> = [
8 { max: 1000, type: '' },
9 { max: 1000000, type: 'K' },
10 { max: 1000000000, type: 'M' }
11 ]
12
13 transform (value: number) {
14 const format = this.dictionary.find(d => value < d.max) || this.dictionary[this.dictionary.length - 1]
15 const calc = Math.floor(value / (format.max / 1000))
16
17 return `${calc}${format.type}`
18 }
19}
diff --git a/client/src/app/shared/misc/utils.ts b/client/src/app/shared/misc/utils.ts
new file mode 100644
index 000000000..df9e0381a
--- /dev/null
+++ b/client/src/app/shared/misc/utils.ts
@@ -0,0 +1,23 @@
1// Thanks: https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
2
3function getParameterByName (name: string, url: string) {
4 if (!url) url = window.location.href
5 name = name.replace(/[\[\]]/g, '\\$&')
6
7 const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)')
8 const results = regex.exec(url)
9
10 if (!results) return null
11 if (!results[2]) return ''
12
13 return decodeURIComponent(results[2].replace(/\+/g, ' '))
14}
15
16function viewportHeight () {
17 return Math.max(document.documentElement.clientHeight, window.innerHeight || 0)
18}
19
20export {
21 viewportHeight,
22 getParameterByName
23}
diff --git a/client/src/app/shared/search/index.ts b/client/src/app/shared/search/index.ts
deleted file mode 100644
index d4016cf89..000000000
--- a/client/src/app/shared/search/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
1export * from './search-field.type'
2export * from './search.component'
3export * from './search.model'
4export * from './search.service'
diff --git a/client/src/app/shared/search/search-field.type.ts b/client/src/app/shared/search/search-field.type.ts
deleted file mode 100644
index 7323d6cc3..000000000
--- a/client/src/app/shared/search/search-field.type.ts
+++ /dev/null
@@ -1 +0,0 @@
1export type SearchField = 'name' | 'account' | 'host' | 'tags'
diff --git a/client/src/app/shared/search/search.component.html b/client/src/app/shared/search/search.component.html
deleted file mode 100644
index 75e9dfa59..000000000
--- a/client/src/app/shared/search/search.component.html
+++ /dev/null
@@ -1,22 +0,0 @@
1<div class="input-group">
2
3 <span class="hidden-xs input-group-addon icon-addon">
4 <span class="glyphicon glyphicon-search"></span>
5 </span>
6
7 <input
8 type="text" id="search-video" name="search-video" class="form-control" placeholder="Search" class="form-control"
9 [(ngModel)]="searchCriteria.value" (keyup.enter)="doSearch()"
10 >
11
12 <div class="input-group-btn" dropdown placement="bottom right">
13 <button id="simple-btn-keyboard-nav" type="button" class="btn btn-default" dropdownToggle>
14 {{ getStringChoice(searchCriteria.field) }} <span class="caret"></span>
15 </button>
16 <ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="simple-btn-keyboard-nav" *dropdownMenu>
17 <li *ngFor="let choice of choiceKeys" class="dropdown-item" role="menu-item">
18 <a class="dropdown-item" href="#" (click)="choose($event, choice)">{{ getStringChoice(choice) }}</a>
19 </li>
20 </ul>
21 </div>
22</div>
diff --git a/client/src/app/shared/search/search.component.scss b/client/src/app/shared/search/search.component.scss
deleted file mode 100644
index 583f9586f..000000000
--- a/client/src/app/shared/search/search.component.scss
+++ /dev/null
@@ -1,51 +0,0 @@
1.icon-addon {
2 background-color: #fff;
3 border-radius: 0;
4 border-color: $header-border-color;
5 border-width: 0 0 1px 0;
6 text-align: right;
7
8 .glyphicon-search {
9 width: 30px;
10 font-size: 20px;
11 }
12}
13
14input, button, .input-group {
15 height: 100%;
16}
17
18input, .input-group-btn {
19 border-radius: 0;
20 border-top: none;
21 border-left: none;
22}
23
24input {
25 height: $header-height;
26 border-right: none;
27 font-weight: bold;
28 box-shadow: none;
29
30 &, &:focus {
31 border-bottom: 1px solid $header-border-color !important;
32 outline: none !important;
33 box-shadow: none !important;
34 }
35}
36
37button {
38
39 &, &:hover, &:focus, &:active, &:visited {
40 background-color: #fff !important;
41 border-color: $header-border-color !important;
42 color: #858585 !important;
43 outline: none !important;
44
45 height: $header-height;
46 border-width: 0 0 1px 0;
47 font-weight: bold;
48 text-decoration: none;
49 box-shadow: none;
50 }
51}
diff --git a/client/src/app/shared/search/search.component.ts b/client/src/app/shared/search/search.component.ts
deleted file mode 100644
index 6ef19c97a..000000000
--- a/client/src/app/shared/search/search.component.ts
+++ /dev/null
@@ -1,69 +0,0 @@
1import { Component, OnInit } from '@angular/core'
2import { Router } from '@angular/router'
3
4import { Search } from './search.model'
5import { SearchField } from './search-field.type'
6import { SearchService } from './search.service'
7
8@Component({
9 selector: 'my-search',
10 templateUrl: './search.component.html',
11 styleUrls: [ './search.component.scss' ]
12})
13
14export class SearchComponent implements OnInit {
15 fieldChoices = {
16 name: 'Name',
17 account: 'Account',
18 host: 'Host',
19 tags: 'Tags'
20 }
21 searchCriteria: Search = {
22 field: 'name',
23 value: ''
24 }
25
26 constructor (private searchService: SearchService, private router: Router) {}
27
28 ngOnInit () {
29 // Subscribe if the search changed
30 // Usually changed by videos list component
31 this.searchService.updateSearch.subscribe(
32 newSearchCriteria => {
33 // Put a field by default
34 if (!newSearchCriteria.field) {
35 newSearchCriteria.field = 'name'
36 }
37
38 this.searchCriteria = newSearchCriteria
39 }
40 )
41 }
42
43 get choiceKeys () {
44 return Object.keys(this.fieldChoices)
45 }
46
47 choose ($event: MouseEvent, choice: SearchField) {
48 $event.preventDefault()
49 $event.stopPropagation()
50
51 this.searchCriteria.field = choice
52
53 if (this.searchCriteria.value) {
54 this.doSearch()
55 }
56 }
57
58 doSearch () {
59 if (this.router.url.indexOf('/videos/list') === -1) {
60 this.router.navigate([ '/videos/list' ])
61 }
62
63 this.searchService.searchUpdated.next(this.searchCriteria)
64 }
65
66 getStringChoice (choiceKey: SearchField) {
67 return this.fieldChoices[choiceKey]
68 }
69}
diff --git a/client/src/app/shared/search/search.model.ts b/client/src/app/shared/search/search.model.ts
deleted file mode 100644
index 174adf2c6..000000000
--- a/client/src/app/shared/search/search.model.ts
+++ /dev/null
@@ -1,6 +0,0 @@
1import { SearchField } from './search-field.type'
2
3export interface Search {
4 field: SearchField
5 value: string
6}
diff --git a/client/src/app/shared/search/search.service.ts b/client/src/app/shared/search/search.service.ts
deleted file mode 100644
index 0480b46bd..000000000
--- a/client/src/app/shared/search/search.service.ts
+++ /dev/null
@@ -1,18 +0,0 @@
1import { Injectable } from '@angular/core'
2import { Subject } from 'rxjs/Subject'
3import { ReplaySubject } from 'rxjs/ReplaySubject'
4
5import { Search } from './search.model'
6
7// This class is needed to communicate between videos/ and search component
8// Remove it when we'll be able to subscribe to router changes
9@Injectable()
10export class SearchService {
11 searchUpdated: Subject<Search>
12 updateSearch: Subject<Search>
13
14 constructor () {
15 this.updateSearch = new Subject<Search>()
16 this.searchUpdated = new ReplaySubject<Search>(1)
17 }
18}
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index 456ce851e..d0e163f69 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -1,25 +1,29 @@
1import { NgModule } from '@angular/core'
2import { HttpClientModule } from '@angular/common/http'
3import { CommonModule } from '@angular/common' 1import { CommonModule } from '@angular/common'
2import { HttpClientModule } from '@angular/common/http'
3import { NgModule } from '@angular/core'
4import { FormsModule, ReactiveFormsModule } from '@angular/forms' 4import { FormsModule, ReactiveFormsModule } from '@angular/forms'
5import { RouterModule } from '@angular/router' 5import { RouterModule } from '@angular/router'
6 6
7import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe'
8import { KeysPipe } from 'angular-pipes/src/object/keys.pipe'
9import { BsDropdownModule } from 'ngx-bootstrap/dropdown' 7import { BsDropdownModule } from 'ngx-bootstrap/dropdown'
10import { ProgressbarModule } from 'ngx-bootstrap/progressbar'
11import { PaginationModule } from 'ngx-bootstrap/pagination'
12import { ModalModule } from 'ngx-bootstrap/modal' 8import { ModalModule } from 'ngx-bootstrap/modal'
13import { DataTableModule } from 'primeng/components/datatable/datatable' 9import { InfiniteScrollModule } from 'ngx-infinite-scroll'
10import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes'
14import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared' 11import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared'
12import { DataTableModule } from 'primeng/components/datatable/datatable'
15 13
16import { AUTH_INTERCEPTOR_PROVIDER } from './auth' 14import { AUTH_INTERCEPTOR_PROVIDER } from './auth'
15import { DeleteButtonComponent } from './misc/delete-button.component'
16import { EditButtonComponent } from './misc/edit-button.component'
17import { FromNowPipe } from './misc/from-now.pipe'
18import { LoaderComponent } from './misc/loader.component'
19import { NumberFormatterPipe } from './misc/number-formatter.pipe'
17import { RestExtractor, RestService } from './rest' 20import { RestExtractor, RestService } from './rest'
18import { SearchComponent, SearchService } from './search'
19import { UserService } from './users' 21import { UserService } from './users'
20import { VideoAbuseService } from './video-abuse' 22import { VideoAbuseService } from './video-abuse'
21import { VideoBlacklistService } from './video-blacklist' 23import { VideoBlacklistService } from './video-blacklist'
22import { LoaderComponent } from './misc/loader.component' 24import { VideoMiniatureComponent } from './video/video-miniature.component'
25import { VideoThumbnailComponent } from './video/video-thumbnail.component'
26import { VideoService } from './video/video.service'
23 27
24@NgModule({ 28@NgModule({
25 imports: [ 29 imports: [
@@ -31,18 +35,21 @@ import { LoaderComponent } from './misc/loader.component'
31 35
32 BsDropdownModule.forRoot(), 36 BsDropdownModule.forRoot(),
33 ModalModule.forRoot(), 37 ModalModule.forRoot(),
34 PaginationModule.forRoot(),
35 ProgressbarModule.forRoot(),
36 38
37 DataTableModule, 39 DataTableModule,
38 PrimeSharedModule 40 PrimeSharedModule,
41 InfiniteScrollModule,
42 NgPipesModule
39 ], 43 ],
40 44
41 declarations: [ 45 declarations: [
42 BytesPipe, 46 LoaderComponent,
43 KeysPipe, 47 VideoThumbnailComponent,
44 SearchComponent, 48 VideoMiniatureComponent,
45 LoaderComponent 49 DeleteButtonComponent,
50 EditButtonComponent,
51 NumberFormatterPipe,
52 FromNowPipe
46 ], 53 ],
47 54
48 exports: [ 55 exports: [
@@ -54,25 +61,30 @@ import { LoaderComponent } from './misc/loader.component'
54 61
55 BsDropdownModule, 62 BsDropdownModule,
56 ModalModule, 63 ModalModule,
57 PaginationModule,
58 ProgressbarModule,
59 DataTableModule, 64 DataTableModule,
60 PrimeSharedModule, 65 PrimeSharedModule,
66 InfiniteScrollModule,
61 BytesPipe, 67 BytesPipe,
62 KeysPipe, 68 KeysPipe,
63 69
64 SearchComponent, 70 LoaderComponent,
65 LoaderComponent 71 VideoThumbnailComponent,
72 VideoMiniatureComponent,
73 DeleteButtonComponent,
74 EditButtonComponent,
75
76 NumberFormatterPipe,
77 FromNowPipe
66 ], 78 ],
67 79
68 providers: [ 80 providers: [
69 AUTH_INTERCEPTOR_PROVIDER, 81 AUTH_INTERCEPTOR_PROVIDER,
70 RestExtractor, 82 RestExtractor,
71 RestService, 83 RestService,
72 SearchService,
73 VideoAbuseService, 84 VideoAbuseService,
74 VideoBlacklistService, 85 VideoBlacklistService,
75 UserService 86 UserService,
87 VideoService
76 ] 88 ]
77}) 89})
78export class SharedModule { } 90export class SharedModule { }
diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts
index b075ab717..b4d13f37c 100644
--- a/client/src/app/shared/users/user.model.ts
+++ b/client/src/app/shared/users/user.model.ts
@@ -1,10 +1,5 @@
1import { 1import { hasUserRight, User as UserServerModel, UserRight, UserRole, VideoChannel } from '../../../../../shared'
2 User as UserServerModel, 2import { Account } from '../account/account.model'
3 UserRole,
4 VideoChannel,
5 UserRight,
6 hasUserRight
7} from '../../../../../shared'
8 3
9export type UserConstructorHash = { 4export type UserConstructorHash = {
10 id: number, 5 id: number,
@@ -14,10 +9,7 @@ export type UserConstructorHash = {
14 videoQuota?: number, 9 videoQuota?: number,
15 displayNSFW?: boolean, 10 displayNSFW?: boolean,
16 createdAt?: Date, 11 createdAt?: Date,
17 account?: { 12 account?: Account,
18 id: number
19 uuid: string
20 },
21 videoChannels?: VideoChannel[] 13 videoChannels?: VideoChannel[]
22} 14}
23export class User implements UserServerModel { 15export class User implements UserServerModel {
@@ -27,10 +19,7 @@ export class User implements UserServerModel {
27 role: UserRole 19 role: UserRole
28 displayNSFW: boolean 20 displayNSFW: boolean
29 videoQuota: number 21 videoQuota: number
30 account: { 22 account: Account
31 id: number
32 uuid: string
33 }
34 videoChannels: VideoChannel[] 23 videoChannels: VideoChannel[]
35 createdAt: Date 24 createdAt: Date
36 25
@@ -61,4 +50,8 @@ export class User implements UserServerModel {
61 hasRight (right: UserRight) { 50 hasRight (right: UserRight) {
62 return hasUserRight(this.role, right) 51 return hasUserRight(this.role, right)
63 } 52 }
53
54 getAvatarPath () {
55 return Account.GET_ACCOUNT_AVATAR_PATH(this.account)
56 }
64} 57}
diff --git a/client/src/app/shared/video/abstract-video-list.html b/client/src/app/shared/video/abstract-video-list.html
new file mode 100644
index 000000000..5761f2c81
--- /dev/null
+++ b/client/src/app/shared/video/abstract-video-list.html
@@ -0,0 +1,20 @@
1<div class="margin-content">
2 <div class="title-page title-page-single">
3 {{ titlePage }}
4 </div>
5
6 <div
7 class="videos"
8 infiniteScroll
9 [infiniteScrollUpDistance]="1.5"
10 [infiniteScrollDistance]="0.5"
11 (scrolled)="onNearOfBottom()"
12 (scrolledUp)="onNearOfTop()"
13 >
14 <my-video-miniature
15 class="ng-animate"
16 *ngFor="let video of videos" [video]="video" [user]="user"
17 >
18 </my-video-miniature>
19 </div>
20</div>
diff --git a/client/src/app/shared/video/abstract-video-list.scss b/client/src/app/shared/video/abstract-video-list.scss
new file mode 100644
index 000000000..52797bc6c
--- /dev/null
+++ b/client/src/app/shared/video/abstract-video-list.scss
@@ -0,0 +1,7 @@
1.videos {
2 text-align: center;
3
4 my-video-miniature {
5 text-align: left;
6 }
7}
diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts
new file mode 100644
index 000000000..ba1635a18
--- /dev/null
+++ b/client/src/app/shared/video/abstract-video-list.ts
@@ -0,0 +1,133 @@
1import { OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { NotificationsService } from 'angular2-notifications'
4import { Observable } from 'rxjs/Observable'
5import { SortField } from './sort-field.type'
6import { VideoPagination } from './video-pagination.model'
7import { Video } from './video.model'
8
9export abstract class AbstractVideoList implements OnInit {
10 pagination: VideoPagination = {
11 currentPage: 1,
12 itemsPerPage: 25,
13 totalItems: null
14 }
15 sort: SortField = '-createdAt'
16 defaultSort: SortField = '-createdAt'
17 videos: Video[] = []
18 loadOnInit = true
19
20 protected notificationsService: NotificationsService
21 protected router: Router
22 protected route: ActivatedRoute
23
24 protected abstract currentRoute: string
25
26 abstract titlePage: string
27 private loadedPages: { [ id: number ]: boolean } = {}
28
29 abstract getVideosObservable (): Observable<{ videos: Video[], totalVideos: number}>
30
31 ngOnInit () {
32 // Subscribe to route changes
33 const routeParams = this.route.snapshot.params
34 this.loadRouteParams(routeParams)
35
36 if (this.loadOnInit === true) this.loadMoreVideos('after')
37 }
38
39 onNearOfTop () {
40 if (this.pagination.currentPage > 1) {
41 this.previousPage()
42 }
43 }
44
45 onNearOfBottom () {
46 if (this.hasMoreVideos()) {
47 this.nextPage()
48 }
49 }
50
51 reloadVideos () {
52 this.videos = []
53 this.loadedPages = {}
54 this.loadMoreVideos('before')
55 }
56
57 loadMoreVideos (where: 'before' | 'after') {
58 if (this.loadedPages[this.pagination.currentPage] === true) return
59
60 const observable = this.getVideosObservable()
61
62 observable.subscribe(
63 ({ videos, totalVideos }) => {
64 // Paging is too high, return to the first one
65 if (this.pagination.currentPage > 1 && totalVideos <= ((this.pagination.currentPage - 1) * this.pagination.itemsPerPage)) {
66 this.pagination.currentPage = 1
67 this.setNewRouteParams()
68 return this.reloadVideos()
69 }
70
71 this.loadedPages[this.pagination.currentPage] = true
72 this.pagination.totalItems = totalVideos
73
74 if (where === 'before') {
75 this.videos = videos.concat(this.videos)
76 } else {
77 this.videos = this.videos.concat(videos)
78 }
79 },
80 error => this.notificationsService.error('Error', error.text)
81 )
82 }
83
84 protected hasMoreVideos () {
85 // No results
86 if (this.pagination.totalItems === 0) return false
87
88 // Not loaded yet
89 if (!this.pagination.totalItems) return true
90
91 const maxPage = this.pagination.totalItems / this.pagination.itemsPerPage
92 return maxPage > this.pagination.currentPage
93 }
94
95 protected previousPage () {
96 this.pagination.currentPage--
97
98 this.setNewRouteParams()
99 this.loadMoreVideos('before')
100 }
101
102 protected nextPage () {
103 this.pagination.currentPage++
104
105 this.setNewRouteParams()
106 this.loadMoreVideos('after')
107 }
108
109 protected buildRouteParams () {
110 // There is always a sort and a current page
111 const params = {
112 sort: this.sort,
113 page: this.pagination.currentPage
114 }
115
116 return params
117 }
118
119 protected loadRouteParams (routeParams: { [ key: string ]: any }) {
120 this.sort = routeParams['sort'] as SortField || this.defaultSort
121
122 if (routeParams['page'] !== undefined) {
123 this.pagination.currentPage = parseInt(routeParams['page'], 10)
124 } else {
125 this.pagination.currentPage = 1
126 }
127 }
128
129 protected setNewRouteParams () {
130 const routeParams = this.buildRouteParams()
131 this.router.navigate([ this.currentRoute, routeParams ])
132 }
133}
diff --git a/client/src/app/videos/shared/sort-field.type.ts b/client/src/app/shared/video/sort-field.type.ts
index 776f360f8..776f360f8 100644
--- a/client/src/app/videos/shared/sort-field.type.ts
+++ b/client/src/app/shared/video/sort-field.type.ts
diff --git a/client/src/app/videos/shared/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts
index 64cb4f847..b96f8f6c8 100644
--- a/client/src/app/videos/shared/video-details.model.ts
+++ b/client/src/app/shared/video/video-details.model.ts
@@ -1,4 +1,5 @@
1import { Video } from './video.model' 1import { Account } from '../../../../../shared/models/accounts'
2import { Video } from '../../shared/video/video.model'
2import { AuthUser } from '../../core' 3import { AuthUser } from '../../core'
3import { 4import {
4 VideoDetails as VideoDetailsServerModel, 5 VideoDetails as VideoDetailsServerModel,
@@ -10,7 +11,7 @@ import {
10} from '../../../../../shared' 11} from '../../../../../shared'
11 12
12export class VideoDetails extends Video implements VideoDetailsServerModel { 13export class VideoDetails extends Video implements VideoDetailsServerModel {
13 account: string 14 accountName: string
14 by: string 15 by: string
15 createdAt: Date 16 createdAt: Date
16 updatedAt: Date 17 updatedAt: Date
@@ -44,6 +45,9 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
44 channel: VideoChannel 45 channel: VideoChannel
45 privacy: VideoPrivacy 46 privacy: VideoPrivacy
46 privacyLabel: string 47 privacyLabel: string
48 account: Account
49 likesPercent: number
50 dislikesPercent: number
47 51
48 constructor (hash: VideoDetailsServerModel) { 52 constructor (hash: VideoDetailsServerModel) {
49 super(hash) 53 super(hash)
@@ -53,6 +57,10 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
53 this.descriptionPath = hash.descriptionPath 57 this.descriptionPath = hash.descriptionPath
54 this.files = hash.files 58 this.files = hash.files
55 this.channel = hash.channel 59 this.channel = hash.channel
60 this.account = hash.account
61
62 this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100
63 this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100
56 } 64 }
57 65
58 getAppropriateMagnetUri (actualDownloadSpeed = 0) { 66 getAppropriateMagnetUri (actualDownloadSpeed = 0) {
@@ -71,7 +79,7 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
71 } 79 }
72 80
73 isRemovableBy (user: AuthUser) { 81 isRemovableBy (user: AuthUser) {
74 return user && this.isLocal === true && (this.account === user.username || user.hasRight(UserRight.REMOVE_ANY_VIDEO)) 82 return user && this.isLocal === true && (this.accountName === user.username || user.hasRight(UserRight.REMOVE_ANY_VIDEO))
75 } 83 }
76 84
77 isBlackistableBy (user: AuthUser) { 85 isBlackistableBy (user: AuthUser) {
@@ -79,6 +87,6 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
79 } 87 }
80 88
81 isUpdatableBy (user: AuthUser) { 89 isUpdatableBy (user: AuthUser) {
82 return user && this.isLocal === true && user.username === this.account 90 return user && this.isLocal === true && user.username === this.accountName
83 } 91 }
84} 92}
diff --git a/client/src/app/videos/shared/video-edit.model.ts b/client/src/app/shared/video/video-edit.model.ts
index 88d23a59f..955255bfa 100644
--- a/client/src/app/videos/shared/video-edit.model.ts
+++ b/client/src/app/shared/video/video-edit.model.ts
@@ -14,18 +14,20 @@ export class VideoEdit {
14 uuid?: string 14 uuid?: string
15 id?: number 15 id?: number
16 16
17 constructor (videoDetails: VideoDetails) { 17 constructor (videoDetails?: VideoDetails) {
18 this.id = videoDetails.id 18 if (videoDetails) {
19 this.uuid = videoDetails.uuid 19 this.id = videoDetails.id
20 this.category = videoDetails.category 20 this.uuid = videoDetails.uuid
21 this.licence = videoDetails.licence 21 this.category = videoDetails.category
22 this.language = videoDetails.language 22 this.licence = videoDetails.licence
23 this.description = videoDetails.description 23 this.language = videoDetails.language
24 this.name = videoDetails.name 24 this.description = videoDetails.description
25 this.tags = videoDetails.tags 25 this.name = videoDetails.name
26 this.nsfw = videoDetails.nsfw 26 this.tags = videoDetails.tags
27 this.channel = videoDetails.channel.id 27 this.nsfw = videoDetails.nsfw
28 this.privacy = videoDetails.privacy 28 this.channel = videoDetails.channel.id
29 this.privacy = videoDetails.privacy
30 }
29 } 31 }
30 32
31 patch (values: Object) { 33 patch (values: Object) {
diff --git a/client/src/app/shared/video/video-miniature.component.html b/client/src/app/shared/video/video-miniature.component.html
new file mode 100644
index 000000000..7ac017235
--- /dev/null
+++ b/client/src/app/shared/video/video-miniature.component.html
@@ -0,0 +1,17 @@
1<div class="video-miniature">
2 <my-video-thumbnail [video]="video" [nsfw]="isVideoNSFWForThisUser()"></my-video-thumbnail>
3
4 <div class="video-miniature-information">
5 <span class="video-miniature-name">
6 <a
7 class="video-miniature-name"
8 [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoNSFWForThisUser() }"
9 >
10 {{ video.name }}
11 </a>
12 </span>
13
14 <span class="video-miniature-created-at-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
15 <span class="video-miniature-account">{{ video.by }}</span>
16 </div>
17</div>
diff --git a/client/src/app/shared/video/video-miniature.component.scss b/client/src/app/shared/video/video-miniature.component.scss
new file mode 100644
index 000000000..37e84897b
--- /dev/null
+++ b/client/src/app/shared/video/video-miniature.component.scss
@@ -0,0 +1,44 @@
1.video-miniature {
2 display: inline-block;
3 padding-right: 15px;
4 margin-bottom: 30px;
5 height: 175px;
6 vertical-align: top;
7
8 .video-miniature-information {
9 width: 200px;
10 margin-top: 2px;
11 line-height: normal;
12
13 .video-miniature-name {
14 display: block;
15 overflow: hidden;
16 text-overflow: ellipsis;
17 white-space: nowrap;
18 font-weight: bold;
19 transition: color 0.2s;
20 font-size: 16px;
21 font-weight: $font-semibold;
22 color: #000;
23
24 &:hover {
25 text-decoration: none;
26 }
27
28 &.blur-filter {
29 filter: blur(3px);
30 padding-left: 4px;
31 }
32 }
33
34 .video-miniature-created-at-views {
35 display: block;
36 font-size: 13px;
37 }
38
39 .video-miniature-account {
40 font-size: 13px;
41 color: #585858;
42 }
43 }
44}
diff --git a/client/src/app/videos/video-list/shared/video-miniature.component.ts b/client/src/app/shared/video/video-miniature.component.ts
index e5a87907b..4d79a74bb 100644
--- a/client/src/app/videos/video-list/shared/video-miniature.component.ts
+++ b/client/src/app/shared/video/video-miniature.component.ts
@@ -1,7 +1,6 @@
1import { Component, Input } from '@angular/core' 1import { Component, Input } from '@angular/core'
2 2import { User } from '../users'
3import { SortField, Video } from '../../shared' 3import { Video } from './video.model'
4import { User } from '../../../shared'
5 4
6@Component({ 5@Component({
7 selector: 'my-video-miniature', 6 selector: 'my-video-miniature',
@@ -9,7 +8,6 @@ import { User } from '../../../shared'
9 templateUrl: './video-miniature.component.html' 8 templateUrl: './video-miniature.component.html'
10}) 9})
11export class VideoMiniatureComponent { 10export class VideoMiniatureComponent {
12 @Input() currentSort: SortField
13 @Input() user: User 11 @Input() user: User
14 @Input() video: Video 12 @Input() video: Video
15 13
diff --git a/client/src/app/videos/shared/video-pagination.model.ts b/client/src/app/shared/video/video-pagination.model.ts
index 9e71769cb..e9db61596 100644
--- a/client/src/app/videos/shared/video-pagination.model.ts
+++ b/client/src/app/shared/video/video-pagination.model.ts
@@ -1,5 +1,5 @@
1export interface VideoPagination { 1export interface VideoPagination {
2 currentPage: number 2 currentPage: number
3 itemsPerPage: number 3 itemsPerPage: number
4 totalItems: number 4 totalItems?: number
5} 5}
diff --git a/client/src/app/shared/video/video-thumbnail.component.html b/client/src/app/shared/video/video-thumbnail.component.html
new file mode 100644
index 000000000..5c698e8f6
--- /dev/null
+++ b/client/src/app/shared/video/video-thumbnail.component.html
@@ -0,0 +1,10 @@
1<a
2 [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name"
3class="video-thumbnail"
4>
5<img [attr.src]="video.thumbnailUrl" alt="video thumbnail" [ngClass]="{ 'blur-filter': nsfw }" />
6
7<div class="video-thumbnail-overlay">
8 {{ video.durationLabel }}
9</div>
10</a>
diff --git a/client/src/app/shared/video/video-thumbnail.component.scss b/client/src/app/shared/video/video-thumbnail.component.scss
new file mode 100644
index 000000000..ab4f9bcb1
--- /dev/null
+++ b/client/src/app/shared/video/video-thumbnail.component.scss
@@ -0,0 +1,28 @@
1.video-thumbnail {
2 display: inline-block;
3 position: relative;
4 border-radius: 4px;
5 overflow: hidden;
6
7 &:hover {
8 text-decoration: none !important;
9 }
10
11 img.blur-filter {
12 filter: blur(5px);
13 transform : scale(1.03);
14 }
15
16 .video-thumbnail-overlay {
17 position: absolute;
18 right: 5px;
19 bottom: 5px;
20 display: inline-block;
21 background-color: rgba(0, 0, 0, 0.7);
22 color: #fff;
23 font-size: 12px;
24 font-weight: $font-bold;
25 border-radius: 3px;
26 padding: 0 5px;
27 }
28}
diff --git a/client/src/app/shared/video/video-thumbnail.component.ts b/client/src/app/shared/video/video-thumbnail.component.ts
new file mode 100644
index 000000000..e543e9903
--- /dev/null
+++ b/client/src/app/shared/video/video-thumbnail.component.ts
@@ -0,0 +1,12 @@
1import { Component, Input } from '@angular/core'
2import { Video } from './video.model'
3
4@Component({
5 selector: 'my-video-thumbnail',
6 styleUrls: [ './video-thumbnail.component.scss' ],
7 templateUrl: './video-thumbnail.component.html'
8})
9export class VideoThumbnailComponent {
10 @Input() video: Video
11 @Input() nsfw = false
12}
diff --git a/client/src/app/videos/shared/video.model.ts b/client/src/app/shared/video/video.model.ts
index 0dd41d71b..d86ef8f92 100644
--- a/client/src/app/videos/shared/video.model.ts
+++ b/client/src/app/shared/video/video.model.ts
@@ -1,8 +1,9 @@
1import { Video as VideoServerModel } from '../../../../../shared' 1import { Video as VideoServerModel } from '../../../../../shared'
2import { User } from '../../shared' 2import { User } from '../'
3import { Account } from '../../../../../shared/models/accounts'
3 4
4export class Video implements VideoServerModel { 5export class Video implements VideoServerModel {
5 account: string 6 accountName: string
6 by: string 7 by: string
7 createdAt: Date 8 createdAt: Date
8 updatedAt: Date 9 updatedAt: Date
@@ -31,6 +32,7 @@ export class Video implements VideoServerModel {
31 likes: number 32 likes: number
32 dislikes: number 33 dislikes: number
33 nsfw: boolean 34 nsfw: boolean
35 account: Account
34 36
35 private static createByString (account: string, serverHost: string) { 37 private static createByString (account: string, serverHost: string) {
36 return account + '@' + serverHost 38 return account + '@' + serverHost
@@ -52,7 +54,7 @@ export class Video implements VideoServerModel {
52 absoluteAPIUrl = window.location.origin 54 absoluteAPIUrl = window.location.origin
53 } 55 }
54 56
55 this.account = hash.account 57 this.accountName = hash.accountName
56 this.createdAt = new Date(hash.createdAt.toString()) 58 this.createdAt = new Date(hash.createdAt.toString())
57 this.categoryLabel = hash.categoryLabel 59 this.categoryLabel = hash.categoryLabel
58 this.category = hash.category 60 this.category = hash.category
@@ -80,7 +82,7 @@ export class Video implements VideoServerModel {
80 this.dislikes = hash.dislikes 82 this.dislikes = hash.dislikes
81 this.nsfw = hash.nsfw 83 this.nsfw = hash.nsfw
82 84
83 this.by = Video.createByString(hash.account, hash.serverHost) 85 this.by = Video.createByString(hash.accountName, hash.serverHost)
84 } 86 }
85 87
86 isVideoNSFWForUser (user: User) { 88 isVideoNSFWForUser (user: User) {
diff --git a/client/src/app/videos/shared/video.service.ts b/client/src/app/shared/video/video.service.ts
index 5d25a26d4..1a0644c3d 100644
--- a/client/src/app/videos/shared/video.service.ts
+++ b/client/src/app/shared/video/video.service.ts
@@ -1,29 +1,23 @@
1import { Injectable } from '@angular/core'
2import { Observable } from 'rxjs/Observable'
3import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' 1import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http'
2import { Injectable } from '@angular/core'
4import 'rxjs/add/operator/catch' 3import 'rxjs/add/operator/catch'
5import 'rxjs/add/operator/map' 4import 'rxjs/add/operator/map'
6 5import { Observable } from 'rxjs/Observable'
6import { Video as VideoServerModel, VideoDetails as VideoDetailsServerModel } from '../../../../../shared'
7import { ResultList } from '../../../../../shared/models/result-list.model'
8import { UserVideoRateUpdate } from '../../../../../shared/models/videos/user-video-rate-update.model'
9import { UserVideoRate } from '../../../../../shared/models/videos/user-video-rate.model'
10import { VideoRateType } from '../../../../../shared/models/videos/video-rate.type'
11import { VideoUpdate } from '../../../../../shared/models/videos/video-update.model'
12import { RestExtractor } from '../rest/rest-extractor.service'
13import { RestService } from '../rest/rest.service'
14import { Search } from '../header/search.model'
15import { UserService } from '../users/user.service'
7import { SortField } from './sort-field.type' 16import { SortField } from './sort-field.type'
8import {
9 RestExtractor,
10 RestService,
11 UserService,
12 Search
13} from '../../shared'
14import { Video } from './video.model'
15import { VideoDetails } from './video-details.model' 17import { VideoDetails } from './video-details.model'
16import { VideoEdit } from './video-edit.model' 18import { VideoEdit } from './video-edit.model'
17import { VideoPagination } from './video-pagination.model' 19import { VideoPagination } from './video-pagination.model'
18import { 20import { Video } from './video.model'
19 UserVideoRate,
20 VideoRateType,
21 VideoUpdate,
22 UserVideoRateUpdate,
23 Video as VideoServerModel,
24 VideoDetails as VideoDetailsServerModel,
25 ResultList
26} from '../../../../../shared'
27 21
28@Injectable() 22@Injectable()
29export class VideoService { 23export class VideoService {
@@ -48,14 +42,17 @@ export class VideoService {
48 } 42 }
49 43
50 updateVideo (video: VideoEdit) { 44 updateVideo (video: VideoEdit) {
51 const language = video.language ? video.language : null 45 const language = video.language || undefined
46 const licence = video.licence || undefined
47 const category = video.category || undefined
48 const description = video.description || undefined
52 49
53 const body: VideoUpdate = { 50 const body: VideoUpdate = {
54 name: video.name, 51 name: video.name,
55 category: video.category, 52 category,
56 licence: video.licence, 53 licence,
57 language, 54 language,
58 description: video.description, 55 description,
59 privacy: video.privacy, 56 privacy: video.privacy,
60 tags: video.tags, 57 tags: video.tags,
61 nsfw: video.nsfw 58 nsfw: video.nsfw
@@ -97,15 +94,14 @@ export class VideoService {
97 .catch((res) => this.restExtractor.handleError(res)) 94 .catch((res) => this.restExtractor.handleError(res))
98 } 95 }
99 96
100 searchVideos (search: Search, videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> { 97 searchVideos (search: string, videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> {
101 const url = VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value) 98 const url = VideoService.BASE_VIDEO_URL + 'search'
102 99
103 const pagination = this.videoPaginationToRestPagination(videoPagination) 100 const pagination = this.videoPaginationToRestPagination(videoPagination)
104 101
105 let params = new HttpParams() 102 let params = new HttpParams()
106 params = this.restService.addRestGetParams(params, pagination, sort) 103 params = this.restService.addRestGetParams(params, pagination, sort)
107 104 params = params.append('search', search)
108 if (search.field) params.set('field', search.field)
109 105
110 return this.authHttp 106 return this.authHttp
111 .get<ResultList<VideoServerModel>>(url, { params }) 107 .get<ResultList<VideoServerModel>>(url, { params })
diff --git a/client/src/app/signup/signup.component.html b/client/src/app/signup/signup.component.html
index b8b7826eb..eb36b29f6 100644
--- a/client/src/app/signup/signup.component.html
+++ b/client/src/app/signup/signup.component.html
@@ -1,7 +1,8 @@
1<div class="row"> 1<div class="margin-content">
2 <div class="content-padding">
3 2
4 <h3>Signup</h3> 3 <div class="title-page title-page-single">
4 Create an account
5 </div>
5 6
6 <div *ngIf="error" class="alert alert-danger">{{ error }}</div> 7 <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
7 8
@@ -10,9 +11,9 @@
10 <label for="username">Username</label> 11 <label for="username">Username</label>
11 <input 12 <input
12 type="text" class="form-control" id="username" placeholder="Username" 13 type="text" class="form-control" id="username" placeholder="Username"
13 formControlName="username" 14 formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }"
14 > 15 >
15 <div *ngIf="formErrors.username" class="alert alert-danger"> 16 <div *ngIf="formErrors.username" class="form-error">
16 {{ formErrors.username }} 17 {{ formErrors.username }}
17 </div> 18 </div>
18 </div> 19 </div>
@@ -21,9 +22,9 @@
21 <label for="email">Email</label> 22 <label for="email">Email</label>
22 <input 23 <input
23 type="text" class="form-control" id="email" placeholder="Email" 24 type="text" class="form-control" id="email" placeholder="Email"
24 formControlName="email" 25 formControlName="email" [ngClass]="{ 'input-error': formErrors['email'] }"
25 > 26 >
26 <div *ngIf="formErrors.email" class="alert alert-danger"> 27 <div *ngIf="formErrors.email" class="form-error">
27 {{ formErrors.email }} 28 {{ formErrors.email }}
28 </div> 29 </div>
29 </div> 30 </div>
@@ -32,15 +33,14 @@
32 <label for="password">Password</label> 33 <label for="password">Password</label>
33 <input 34 <input
34 type="password" class="form-control" id="password" placeholder="Password" 35 type="password" class="form-control" id="password" placeholder="Password"
35 formControlName="password" 36 formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
36 > 37 >
37 <div *ngIf="formErrors.password" class="alert alert-danger"> 38 <div *ngIf="formErrors.password" class="form-error">
38 {{ formErrors.password }} 39 {{ formErrors.password }}
39 </div> 40 </div>
40 </div> 41 </div>
41 42
42 <input type="submit" value="Signup" class="btn btn-default" [disabled]="!form.valid"> 43 <input type="submit" value="Signup" [disabled]="!form.valid">
43 </form> 44 </form>
44 45
45 </div>
46</div> 46</div>
diff --git a/client/src/app/signup/signup.component.scss b/client/src/app/signup/signup.component.scss
new file mode 100644
index 000000000..3b4326de4
--- /dev/null
+++ b/client/src/app/signup/signup.component.scss
@@ -0,0 +1,9 @@
1input:not([type=submit]) {
2 @include peertube-input-text(340px);
3 display: block;
4}
5
6input[type=submit] {
7 @include peertube-button;
8 @include orange-button;
9}
diff --git a/client/src/app/signup/signup.component.ts b/client/src/app/signup/signup.component.ts
index 28e1ed0a8..13390a32a 100644
--- a/client/src/app/signup/signup.component.ts
+++ b/client/src/app/signup/signup.component.ts
@@ -16,7 +16,8 @@ import { UserCreate } from '../../../../shared'
16 16
17@Component({ 17@Component({
18 selector: 'my-signup', 18 selector: 'my-signup',
19 templateUrl: './signup.component.html' 19 templateUrl: './signup.component.html',
20 styleUrls: [ './signup.component.scss' ]
20}) 21})
21export class SignupComponent extends FormReactive implements OnInit { 22export class SignupComponent extends FormReactive implements OnInit {
22 error: string = null 23 error: string = null
diff --git a/client/src/app/videos/shared/video-description.component.html b/client/src/app/videos/+video-edit/shared/video-description.component.html
index 7a228857c..5d05467be 100644
--- a/client/src/app/videos/shared/video-description.component.html
+++ b/client/src/app/videos/+video-edit/shared/video-description.component.html
@@ -1,6 +1,6 @@
1<textarea 1<textarea
2 [(ngModel)]="description" (ngModelChange)="onModelChange()" 2 [(ngModel)]="description" (ngModelChange)="onModelChange()"
3 id="description" class="form-control" placeholder="My super video"> 3 id="description" name="description">
4</textarea> 4</textarea>
5 5
6<tabset #staticTabs class="previews"> 6<tabset #staticTabs class="previews">
diff --git a/client/src/app/videos/+video-edit/shared/video-description.component.scss b/client/src/app/videos/+video-edit/shared/video-description.component.scss
new file mode 100644
index 000000000..2a4c8d189
--- /dev/null
+++ b/client/src/app/videos/+video-edit/shared/video-description.component.scss
@@ -0,0 +1,24 @@
1textarea {
2 @include peertube-input-text(100%);
3
4 padding: 5px 15px;
5 font-size: 15px;
6 height: 150px;
7 margin-bottom: 15px;
8}
9
10/deep/ {
11 .nav-link {
12 display: flex !important;
13 align-items: center;
14 height: 30px !important;
15 padding: 0 15px !important;
16 }
17
18 .tab-content {
19 min-height: 75px;
20 padding: 15px;
21 font-size: 15px;
22 }
23}
24
diff --git a/client/src/app/videos/shared/video-description.component.ts b/client/src/app/videos/+video-edit/shared/video-description.component.ts
index d9ffb7800..9b77a27e6 100644
--- a/client/src/app/videos/shared/video-description.component.ts
+++ b/client/src/app/videos/+video-edit/shared/video-description.component.ts
@@ -1,12 +1,10 @@
1import { Component, forwardRef, Input, OnInit } 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 { Subject } from 'rxjs/Subject' 3import { truncate } from 'lodash'
4import 'rxjs/add/operator/debounceTime' 4import 'rxjs/add/operator/debounceTime'
5import 'rxjs/add/operator/distinctUntilChanged' 5import 'rxjs/add/operator/distinctUntilChanged'
6 6import { Subject } from 'rxjs/Subject'
7import { truncate } from 'lodash' 7import { MarkdownService } from '../../shared'
8
9import { MarkdownService } from './markdown.service'
10 8
11@Component({ 9@Component({
12 selector: 'my-video-description', 10 selector: 'my-video-description',
@@ -62,6 +60,8 @@ export class VideoDescriptionComponent implements ControlValueAccessor, OnInit {
62 } 60 }
63 61
64 private updateDescriptionPreviews () { 62 private updateDescriptionPreviews () {
63 if (!this.description) return
64
65 this.truncatedDescriptionHTML = this.markdownService.markdownToHTML(truncate(this.description, { length: 250 })) 65 this.truncatedDescriptionHTML = this.markdownService.markdownToHTML(truncate(this.description, { length: 250 }))
66 this.descriptionHTML = this.markdownService.markdownToHTML(this.description) 66 this.descriptionHTML = this.markdownService.markdownToHTML(this.description)
67 } 67 }
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
new file mode 100644
index 000000000..8c071ce12
--- /dev/null
+++ b/client/src/app/videos/+video-edit/shared/video-edit.component.html
@@ -0,0 +1,86 @@
1<div class="video-edit row" [formGroup]="form">
2
3 <div class="col-md-8">
4 <div class="form-group">
5 <label for="name">Title</label>
6 <input type="text" id="name" formControlName="name" />
7 <div *ngIf="formErrors.name" class="form-error">
8 {{ formErrors.name }}
9 </div>
10 </div>
11
12 <div class="form-group">
13 <label class="label-tags">Tags</label> <span class="little-information">(press enter to add the tag)</span>
14 <tag-input
15 [ngModel]="tags" [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
16 formControlName="tags" maxItems="5" modelAsStrings="true"
17 ></tag-input>
18 </div>
19
20 <div class="form-group">
21 <label for="description">Description</label>
22 <my-video-description formControlName="description"></my-video-description>
23
24 <div *ngIf="formErrors.description" class="form-error">
25 {{ formErrors.description }}
26 </div>
27 </div>
28 </div>
29
30 <div class="col-md-4">
31 <div class="form-group">
32 <label for="category">Category</label>
33 <select id="category" formControlName="category">
34 <option></option>
35 <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option>
36 </select>
37
38 <div *ngIf="formErrors.category" class="form-error">
39 {{ formErrors.category }}
40 </div>
41 </div>
42
43 <div class="form-group">
44 <label for="licence">Licence</label>
45 <select id="licence" formControlName="licence">
46 <option></option>
47 <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option>
48 </select>
49
50 <div *ngIf="formErrors.licence" class="form-error">
51 {{ formErrors.licence }}
52 </div>
53 </div>
54
55 <div class="form-group">
56 <label for="language">Language</label>
57 <select id="language" formControlName="language">
58 <option></option>
59 <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option>
60 </select>
61
62 <div *ngIf="formErrors.language" class="form-error">
63 {{ formErrors.language }}
64 </div>
65 </div>
66
67 <div class="form-group">
68 <label for="privacy">Privacy</label>
69 <select id="privacy" formControlName="privacy">
70
71 <option></option>
72 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
73 </select>
74
75 <div *ngIf="formErrors.privacy" class="form-error">
76 {{ formErrors.privacy }}
77 </div>
78 </div>
79
80 <div class="form-group form-group-checkbox">
81 <input type="checkbox" id="nsfw" formControlName="nsfw" />
82 <label for="nsfw">This video contains mature or explicit content</label>
83 </div>
84
85 </div>
86</div>
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.scss b/client/src/app/videos/+video-edit/shared/video-edit.component.scss
index 9ee0c520c..d363499ce 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.component.scss
+++ b/client/src/app/videos/+video-edit/shared/video-edit.component.scss
@@ -1,48 +1,126 @@
1.btn-file { 1.video-edit {
2 position: relative; 2 height: 100%;
3 overflow: hidden; 3
4 display: block; 4 .form-group {
5 margin-bottom: 25px;
6 }
7
8 input {
9 @include peertube-input-text(100%);
10 display: block;
11
12 &[type=checkbox] {
13 outline: 0;
14 }
15 }
16
17 select {
18 @include peertube-select(100%);
19 }
20
21 input, select {
22 font-size: 15px
23 }
24
25 .form-group-checkbox {
26 display: flex;
27 align-items: center;
28
29 label {
30 font-weight: $font-regular;
31 margin: 0;
32 }
33
34 input {
35 width: 10px;
36 margin-right: 10px;
37 }
38 }
5} 39}
6 40
7.btn-file input[type=file] { 41.submit-container {
8 position: absolute;
9 top: 0;
10 right: 0;
11 min-width: 100%;
12 min-height: 100%;
13 font-size: 100px;
14 text-align: right; 42 text-align: right;
15 filter: alpha(opacity=0); 43 position: relative;
16 opacity: 0; 44 bottom: $button-height;
17 outline: none;
18 background: white;
19 cursor: inherit;
20 display: block;
21}
22 45
23.form-group { 46 .message-submit {
24 margin-bottom: 10px; 47 display: inline-block;
25} 48 margin-right: 25px;
49
50 color: #585858;
51 font-size: 15px;
52 }
53
54 .submit-button {
55 @include peertube-button;
56 @include orange-button;
57
58 display: inline-block;
26 59
27div.tags { 60 input {
28 height: 40px; 61 cursor: inherit;
29 font-size: 20px; 62 background-color: inherit;
30 margin-top: 20px; 63 border: none;
64 padding: 0;
65 outline: 0;
66 }
31 67
32 .tag { 68 .icon.icon-validate {
33 margin-right: 10px; 69 @include icon(20px);
34 70
35 .remove { 71 cursor: inherit;
36 cursor: pointer; 72 position: relative;
73 top: -1px;
74 margin-right: 4px;
75 background-image: url('../../../../assets/images/global/validate.svg');
37 } 76 }
38 } 77 }
39} 78}
40 79
41div.file-to-upload { 80/deep/ {
42 height: 40px; 81 .ng2-tag-input {
82 border: none !important;
83 }
43 84
44 .glyphicon-remove { 85 .ng2-tags-container {
45 cursor: pointer; 86 display: flex;
87 align-items: center;
88 border: 1px solid #C6C6C6;
89 border-radius: 3px;
90 padding: 5px !important;
91 }
92
93 tag {
94 background-color: #E5E5E5 !important;
95 border-radius: 3px !important;
96 font-size: 15px !important;
97 color: #000 !important;
98 height: 30px !important;
99 line-height: 30px !important;
100 margin: 0 5px 0 0 !important;
101 cursor: default !important;
102 padding: 0 8px 0 10px !important;
103
104 div {
105 height: 100% !important;
106 }
107 }
108
109 delete-icon {
110 cursor: pointer !important;
111 height: auto !important;
112 vertical-align: middle !important;
113 padding-left: 6px !important;
114
115 svg {
116 height: auto !important;
117 vertical-align: middle !important;
118 fill: #585858 !important;
119 }
120
121 &:hover {
122 transform: none !important;
123 }
46 } 124 }
47} 125}
48 126
@@ -50,7 +128,3 @@ div.file-to-upload {
50 font-size: 0.8em; 128 font-size: 0.8em;
51 font-style: italic; 129 font-style: italic;
52} 130}
53
54.label-tags {
55 margin-bottom: 0;
56}
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
new file mode 100644
index 000000000..5b1cc3f9c
--- /dev/null
+++ b/client/src/app/videos/+video-edit/shared/video-edit.component.ts
@@ -0,0 +1,83 @@
1import { Component, Input, OnInit } from '@angular/core'
2import { FormBuilder, FormControl, FormGroup } from '@angular/forms'
3import { ActivatedRoute, Router } from '@angular/router'
4import { NotificationsService } from 'angular2-notifications'
5import { ServerService } from 'app/core'
6import { VideoEdit } from 'app/shared/video/video-edit.model'
7import 'rxjs/add/observable/forkJoin'
8import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum'
9import {
10 ValidatorMessage,
11 VIDEO_CATEGORY,
12 VIDEO_DESCRIPTION,
13 VIDEO_LANGUAGE,
14 VIDEO_LICENCE,
15 VIDEO_NAME,
16 VIDEO_PRIVACY,
17 VIDEO_TAGS
18} from '../../../shared/forms/form-validators'
19
20@Component({
21 selector: 'my-video-edit',
22 styleUrls: [ './video-edit.component.scss' ],
23 templateUrl: './video-edit.component.html'
24})
25
26export class VideoEditComponent implements OnInit {
27 @Input() form: FormGroup
28 @Input() formErrors: { [ id: string ]: string } = {}
29 @Input() validationMessages: ValidatorMessage = {}
30 @Input() videoPrivacies = []
31
32 tags: string[] = []
33 videoCategories = []
34 videoLicences = []
35 videoLanguages = []
36 video: VideoEdit
37
38 tagValidators = VIDEO_TAGS.VALIDATORS
39 tagValidatorsMessages = VIDEO_TAGS.MESSAGES
40
41 error: string = null
42
43 constructor (
44 private formBuilder: FormBuilder,
45 private route: ActivatedRoute,
46 private router: Router,
47 private notificationsService: NotificationsService,
48 private serverService: ServerService
49 ) { }
50
51 updateForm () {
52 this.formErrors['name'] = ''
53 this.formErrors['privacy'] = ''
54 this.formErrors['category'] = ''
55 this.formErrors['licence'] = ''
56 this.formErrors['language'] = ''
57 this.formErrors['description'] = ''
58
59 this.validationMessages['name'] = VIDEO_NAME.MESSAGES
60 this.validationMessages['privacy'] = VIDEO_PRIVACY.MESSAGES
61 this.validationMessages['category'] = VIDEO_CATEGORY.MESSAGES
62 this.validationMessages['licence'] = VIDEO_LICENCE.MESSAGES
63 this.validationMessages['language'] = VIDEO_LANGUAGE.MESSAGES
64 this.validationMessages['description'] = VIDEO_DESCRIPTION.MESSAGES
65
66 this.form.addControl('name', new FormControl('', VIDEO_NAME.VALIDATORS))
67 this.form.addControl('privacy', new FormControl('', VIDEO_PRIVACY.VALIDATORS))
68 this.form.addControl('nsfw', new FormControl(false))
69 this.form.addControl('category', new FormControl('', VIDEO_CATEGORY.VALIDATORS))
70 this.form.addControl('licence', new FormControl('', VIDEO_LICENCE.VALIDATORS))
71 this.form.addControl('language', new FormControl('', VIDEO_LANGUAGE.VALIDATORS))
72 this.form.addControl('description', new FormControl('', VIDEO_DESCRIPTION.VALIDATORS))
73 this.form.addControl('tags', new FormControl(''))
74 }
75
76 ngOnInit () {
77 this.updateForm()
78
79 this.videoCategories = this.serverService.getVideoCategories()
80 this.videoLicences = this.serverService.getVideoLicences()
81 this.videoLanguages = this.serverService.getVideoLanguages()
82 }
83}
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.module.ts b/client/src/app/videos/+video-edit/shared/video-edit.module.ts
index c64cea920..ce106d82f 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.module.ts
+++ b/client/src/app/videos/+video-edit/shared/video-edit.module.ts
@@ -3,8 +3,10 @@ import { NgModule } from '@angular/core'
3import { TagInputModule } from 'ngx-chips' 3import { TagInputModule } from 'ngx-chips'
4import { TabsModule } from 'ngx-bootstrap/tabs' 4import { TabsModule } from 'ngx-bootstrap/tabs'
5 5
6import { VideoService, MarkdownService, VideoDescriptionComponent } from '../../shared' 6import { MarkdownService } from '../../shared'
7import { SharedModule } from '../../../shared' 7import { SharedModule } from '../../../shared'
8import { VideoDescriptionComponent } from './video-description.component'
9import { VideoEditComponent } from './video-edit.component'
8 10
9@NgModule({ 11@NgModule({
10 imports: [ 12 imports: [
@@ -15,18 +17,19 @@ import { SharedModule } from '../../../shared'
15 ], 17 ],
16 18
17 declarations: [ 19 declarations: [
18 VideoDescriptionComponent 20 VideoDescriptionComponent,
21 VideoEditComponent
19 ], 22 ],
20 23
21 exports: [ 24 exports: [
22 TagInputModule, 25 TagInputModule,
23 TabsModule, 26 TabsModule,
24 27
25 VideoDescriptionComponent 28 VideoDescriptionComponent,
29 VideoEditComponent
26 ], 30 ],
27 31
28 providers: [ 32 providers: [
29 VideoService,
30 MarkdownService 33 MarkdownService
31 ] 34 ]
32}) 35})
diff --git a/client/src/app/videos/+video-edit/video-add.component.html b/client/src/app/videos/+video-edit/video-add.component.html
index b4e0f9f7c..a6f2bf6f2 100644
--- a/client/src/app/videos/+video-edit/video-add.component.html
+++ b/client/src/app/videos/+video-edit/video-add.component.html
@@ -1,141 +1,53 @@
1<div class="row"> 1<div class="margin-content">
2 <div class="content-padding"> 2 <div class="title-page title-page-single">
3 Upload your video
4 </div>
3 5
4 <h3>Upload a video</h3> 6 <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
5 7
6 <div *ngIf="error !== undefined" class="alert alert-danger">{{ error }}</div> 8 <div *ngIf="!isUploadingVideo" class="upload-video-container">
9 <div class="upload-video">
10 <div class="icon icon-upload"></div>
7 11
8 <form novalidate [formGroup]="form"> 12 <div class="button-file">
9 <div class="form-group"> 13 <span>Select the file to upload</span>
10 <label for="name">Name</label> 14 <input #videofileInput type="file" name="videofile" id="videofile" (change)="fileChange()" />
11 <input
12 type="text" class="form-control" id="name"
13 formControlName="name"
14 >
15 <div *ngIf="formErrors.name" class="alert alert-danger">
16 {{ formErrors.name }}
17 </div>
18 </div> 15 </div>
19 16
20 <div class="form-group"> 17 <div class="form-group">
21 <label for="privacy">Privacy</label> 18 <select [(ngModel)]="firstStepPrivacyId">
22 <select class="form-control" id="privacy" formControlName="privacy">
23 <option></option>
24 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option> 19 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
25 </select> 20 </select>
26
27 <div *ngIf="formErrors.privacy" class="alert alert-danger">
28 {{ formErrors.privacy }}
29 </div>
30 </div>
31
32 <div class="form-group">
33 <input
34 type="checkbox" id="nsfw"
35 formControlName="nsfw"
36 >
37 <label for="nsfw">This video contains mature or explicit content</label>
38 </div> 21 </div>
39 22
40 <div class="form-group"> 23 <div class="form-group">
41 <label for="category">Channel</label> 24 <select [(ngModel)]="firstStepChannelId">
42 <select class="form-control" id="channelId" formControlName="channelId">
43 <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option> 25 <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
44 </select> 26 </select>
45
46 <div *ngIf="formErrors.channelId" class="alert alert-danger">
47 {{ formErrors.channelId }}
48 </div>
49 </div>
50
51 <div class="form-group">
52 <label for="category">Category</label>
53 <select class="form-control" id="category" formControlName="category">
54 <option></option>
55 <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option>
56 </select>
57
58 <div *ngIf="formErrors.category" class="alert alert-danger">
59 {{ formErrors.category }}
60 </div>
61 </div>
62
63 <div class="form-group">
64 <label for="licence">Licence</label>
65 <select class="form-control" id="licence" formControlName="licence">
66 <option></option>
67 <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option>
68 </select>
69
70 <div *ngIf="formErrors.licence" class="alert alert-danger">
71 {{ formErrors.licence }}
72 </div>
73 </div>
74
75 <div class="form-group">
76 <label for="language">Language</label>
77 <select class="form-control" id="language" formControlName="language">
78 <option></option>
79 <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option>
80 </select>
81
82 <div *ngIf="formErrors.language" class="alert alert-danger">
83 {{ formErrors.language }}
84 </div>
85 </div>
86
87 <div class="form-group">
88 <label class="label-tags">Tags</label> <span class="little-information">(press enter to add the tag)</span>
89 <tag-input
90 [ngModel]="tags" [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
91 formControlName="tags" maxItems="5" modelAsStrings="true"
92 ></tag-input>
93 </div>
94
95 <div class="form-group">
96 <label for="videofile">File</label>
97 <div class="btn btn-default btn-file">
98 <span>Select the video...</span>
99 <input #videofileInput type="file" name="videofile" id="videofile" (change)="fileChange($event)" />
100 <input type="hidden" name="videofileHidden" formControlName="videofile"/>
101 </div>
102 </div> 27 </div>
28 </div>
29 </div>
103 30
104 <div class="file-to-upload"> 31 <p-progressBar
105 <div class="file" *ngIf="filename"> 32 *ngIf="isUploadingVideo" [value]="videoUploadPercents"
106 <span class="filename">{{ filename }}</span> 33 [ngClass]="{ processing: videoUploadPercents === 100 && videoUploaded === false }"
107 <span class="glyphicon glyphicon-remove" (click)="removeFile()"></span> 34 ></p-progressBar>
108 </div>
109 </div>
110 35
111 <div *ngIf="formErrors.videofile" class="alert alert-danger"> 36 <!-- Hidden because we need to load the component -->
112 {{ formErrors.videofile }} 37 <form [hidden]="!isUploadingVideo" novalidate [formGroup]="form">
113 </div> 38 <my-video-edit
39 [form]="form" [formErrors]="formErrors"
40 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies"
41 ></my-video-edit>
114 42
115 <div class="form-group">
116 <label for="description">Description</label>
117 <my-video-description formControlName="description"></my-video-description>
118 43
119 <div *ngIf="formErrors.description" class="alert alert-danger"> 44 <div class="submit-container">
120 {{ formErrors.description }} 45 <div *ngIf="videoUploaded === false" class="message-submit">Publish will be available when upload is finished</div>
121 </div>
122 </div>
123 46
124 <div class="progress"> 47 <div class="submit-button" (click)="updateSecondStep()" [ngClass]="{ disabled: !form.valid || videoUploaded !== true }">
125 <progressbar [value]="progressPercent" max="100"> 48 <span class="icon icon-validate"></span>
126 <ng-template [ngIf]="progressPercent === 100"> 49 <input type="button" value="Publish" />
127 <span class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></span>
128 Server is processing the video
129 </ng-template>
130 </progressbar>
131 </div> 50 </div>
132 51 </div>
133 <div class="form-group"> 52 </form>
134 <input
135 type="button" value="Upload" class="btn btn-default form-control"
136 (click)="upload()"
137 >
138 </div>
139 </form>
140 </div>
141</div> 53</div>
diff --git a/client/src/app/videos/+video-edit/video-add.component.scss b/client/src/app/videos/+video-edit/video-add.component.scss
new file mode 100644
index 000000000..39673b4b7
--- /dev/null
+++ b/client/src/app/videos/+video-edit/video-add.component.scss
@@ -0,0 +1,96 @@
1.upload-video-container {
2 border-radius: 3px;
3 background-color: #F7F7F7;
4 border: 3px solid #EAEAEA;
5 width: 100%;
6 height: 440px;
7 text-align: center;
8 margin-top: 40px;
9 display: flex;
10 justify-content: center;
11 align-items: center;
12
13 .upload-video {
14 display: flex;
15 flex-direction: column;
16 align-items: center;
17
18 .icon.icon-upload {
19 @include icon(90px);
20 margin-bottom: 25px;
21 cursor: default;
22
23 background-image: url('../../../assets/images/video/upload.svg');
24 }
25
26 .button-file {
27 position: relative;
28 overflow: hidden;
29 display: inline-block;
30 margin-bottom: 70px;
31
32 @include peertube-button;
33 @include orange-button;
34
35 input[type=file] {
36 position: absolute;
37 top: 0;
38 right: 0;
39 min-width: 100%;
40 min-height: 100%;
41 font-size: 100px;
42 text-align: right;
43 filter: alpha(opacity=0);
44 opacity: 0;
45 outline: none;
46 background: white;
47 cursor: inherit;
48 display: block;
49 }
50 }
51
52 select {
53 @include peertube-select(auto);
54
55 display: inline-block;
56 font-size: 15px
57 }
58 }
59}
60
61p-progressBar {
62 /deep/ .ui-progressbar {
63 margin-top: 25px !important;
64 margin-bottom: 40px !important;
65 font-size: 15px !important;
66 color: #fff !important;
67 height: 30px !important;
68 line-height: 30px !important;
69 border-radius: 3px !important;
70 background-color: rgba(11, 204, 41, 0.16) !important;
71
72 .ui-progressbar-value {
73 background-color: #0BCC29 !important;
74 }
75
76 .ui-progressbar-label {
77 text-align: left;
78 padding-left: 18px;
79 margin-top: 0 !important;
80 }
81 }
82
83 &.processing {
84 /deep/ .ui-progressbar-label {
85 // Same color as background to hide "100%"
86 color: rgba(11, 204, 41, 0.16) !important;
87
88 &::before {
89 content: 'Processing...';
90 color: #fff;
91 }
92 }
93 }
94}
95
96
diff --git a/client/src/app/videos/+video-edit/video-add.component.ts b/client/src/app/videos/+video-edit/video-add.component.ts
index 1704cf486..2bbc3de17 100644
--- a/client/src/app/videos/+video-edit/video-add.component.ts
+++ b/client/src/app/videos/+video-edit/video-add.component.ts
@@ -1,68 +1,42 @@
1import { HttpEventType, HttpResponse } from '@angular/common/http'
1import { Component, OnInit, ViewChild } from '@angular/core' 2import { Component, OnInit, ViewChild } from '@angular/core'
2import { FormBuilder, FormGroup } from '@angular/forms' 3import { FormBuilder, FormGroup } from '@angular/forms'
3import { Router } from '@angular/router' 4import { Router } from '@angular/router'
4
5import { NotificationsService } from 'angular2-notifications' 5import { NotificationsService } from 'angular2-notifications'
6 6import { VideoService } from 'app/shared/video/video.service'
7import {
8 FormReactive,
9 VIDEO_NAME,
10 VIDEO_CATEGORY,
11 VIDEO_LICENCE,
12 VIDEO_LANGUAGE,
13 VIDEO_DESCRIPTION,
14 VIDEO_TAGS,
15 VIDEO_CHANNEL,
16 VIDEO_FILE,
17 VIDEO_PRIVACY
18} from '../../shared'
19import { AuthService, ServerService } from '../../core'
20import { VideoService } from '../shared'
21import { VideoCreate } from '../../../../../shared' 7import { VideoCreate } from '../../../../../shared'
22import { HttpEventType, HttpResponse } from '@angular/common/http' 8import { VideoPrivacy } from '../../../../../shared/models/videos'
9import { AuthService, ServerService } from '../../core'
10import { FormReactive } from '../../shared'
11import { ValidatorMessage } from '../../shared/forms/form-validators'
12import { VideoEdit } from '../../shared/video/video-edit.model'
23 13
24@Component({ 14@Component({
25 selector: 'my-videos-add', 15 selector: 'my-videos-add',
26 styleUrls: [ './shared/video-edit.component.scss' ], 16 templateUrl: './video-add.component.html',
27 templateUrl: './video-add.component.html' 17 styleUrls: [
18 './shared/video-edit.component.scss',
19 './video-add.component.scss'
20 ]
28}) 21})
29 22
30export class VideoAddComponent extends FormReactive implements OnInit { 23export class VideoAddComponent extends FormReactive implements OnInit {
31 @ViewChild('videofileInput') videofileInput 24 @ViewChild('videofileInput') videofileInput
32 25
33 progressPercent = 0 26 isUploadingVideo = false
34 tags: string[] = [] 27 videoUploaded = false
35 videoCategories = [] 28 videoUploadPercents = 0
36 videoLicences = [] 29 videoUploadedId = 0
37 videoLanguages = []
38 videoPrivacies = []
39 userVideoChannels = []
40
41 tagValidators = VIDEO_TAGS.VALIDATORS
42 tagValidatorsMessages = VIDEO_TAGS.MESSAGES
43 30
44 error: string 31 error: string = null
45 form: FormGroup 32 form: FormGroup
46 formErrors = { 33 formErrors: { [ id: string ]: string } = {}
47 name: '', 34 validationMessages: ValidatorMessage = {}
48 privacy: '', 35
49 category: '', 36 userVideoChannels = []
50 licence: '', 37 videoPrivacies = []
51 language: '', 38 firstStepPrivacyId = 0
52 channelId: '', 39 firstStepChannelId = 0
53 description: '',
54 videofile: ''
55 }
56 validationMessages = {
57 name: VIDEO_NAME.MESSAGES,
58 privacy: VIDEO_PRIVACY.MESSAGES,
59 category: VIDEO_CATEGORY.MESSAGES,
60 licence: VIDEO_LICENCE.MESSAGES,
61 language: VIDEO_LANGUAGE.MESSAGES,
62 channelId: VIDEO_CHANNEL.MESSAGES,
63 description: VIDEO_DESCRIPTION.MESSAGES,
64 videofile: VIDEO_FILE.MESSAGES
65 }
66 40
67 constructor ( 41 constructor (
68 private formBuilder: FormBuilder, 42 private formBuilder: FormBuilder,
@@ -75,35 +49,23 @@ export class VideoAddComponent extends FormReactive implements OnInit {
75 super() 49 super()
76 } 50 }
77 51
78 get filename () {
79 return this.form.value['videofile']
80 }
81
82 buildForm () { 52 buildForm () {
83 this.form = this.formBuilder.group({ 53 this.form = this.formBuilder.group({})
84 name: [ '', VIDEO_NAME.VALIDATORS ],
85 nsfw: [ false ],
86 privacy: [ '', VIDEO_PRIVACY.VALIDATORS ],
87 category: [ '', VIDEO_CATEGORY.VALIDATORS ],
88 licence: [ '', VIDEO_LICENCE.VALIDATORS ],
89 language: [ '', VIDEO_LANGUAGE.VALIDATORS ],
90 channelId: [ '', VIDEO_CHANNEL.VALIDATORS ],
91 description: [ '', VIDEO_DESCRIPTION.VALIDATORS ],
92 videofile: [ '', VIDEO_FILE.VALIDATORS ],
93 tags: [ '' ]
94 })
95
96 this.form.valueChanges.subscribe(data => this.onValueChanged(data)) 54 this.form.valueChanges.subscribe(data => this.onValueChanged(data))
97 } 55 }
98 56
99 ngOnInit () { 57 ngOnInit () {
100 this.videoCategories = this.serverService.getVideoCategories()
101 this.videoLicences = this.serverService.getVideoLicences()
102 this.videoLanguages = this.serverService.getVideoLanguages()
103 this.videoPrivacies = this.serverService.getVideoPrivacies()
104
105 this.buildForm() 58 this.buildForm()
106 59
60 this.serverService.videoPrivaciesLoaded
61 .subscribe(
62 () => {
63 this.videoPrivacies = this.serverService.getVideoPrivacies()
64
65 // Public by default
66 this.firstStepPrivacyId = VideoPrivacy.PUBLIC
67 })
68
107 this.authService.userInformationLoaded 69 this.authService.userInformationLoaded
108 .subscribe( 70 .subscribe(
109 () => { 71 () => {
@@ -114,21 +76,13 @@ export class VideoAddComponent extends FormReactive implements OnInit {
114 if (Array.isArray(videoChannels) === false) return 76 if (Array.isArray(videoChannels) === false) return
115 77
116 this.userVideoChannels = videoChannels.map(v => ({ id: v.id, label: v.name })) 78 this.userVideoChannels = videoChannels.map(v => ({ id: v.id, label: v.name }))
117 79 this.firstStepChannelId = this.userVideoChannels[0].id
118 this.form.patchValue({ channelId: this.userVideoChannels[0].id })
119 } 80 }
120 ) 81 )
121 } 82 }
122 83
123 // The goal is to keep reactive form validation (required field) 84 fileChange () {
124 // https://stackoverflow.com/a/44238894 85 this.uploadFirstStep()
125 fileChange ($event) {
126 this.form.controls['videofile'].setValue($event.target.files[0].name)
127 }
128
129 removeFile () {
130 this.videofileInput.nativeElement.value = ''
131 this.form.controls['videofile'].setValue('')
132 } 86 }
133 87
134 checkForm () { 88 checkForm () {
@@ -137,62 +91,72 @@ export class VideoAddComponent extends FormReactive implements OnInit {
137 return this.form.valid 91 return this.form.valid
138 } 92 }
139 93
140 upload () { 94 uploadFirstStep () {
141 if (this.checkForm() === false) {
142 return
143 }
144
145 const formValue: VideoCreate = this.form.value
146
147 const name = formValue.name
148 const privacy = formValue.privacy
149 const nsfw = formValue.nsfw
150 const category = formValue.category
151 const licence = formValue.licence
152 const language = formValue.language
153 const channelId = formValue.channelId
154 const description = formValue.description
155 const tags = formValue.tags
156 const videofile = this.videofileInput.nativeElement.files[0] 95 const videofile = this.videofileInput.nativeElement.files[0]
96 const name = videofile.name.replace(/\.[^/.]+$/, '')
97 const privacy = this.firstStepPrivacyId.toString()
98 const nsfw = false
99 const channelId = this.firstStepChannelId.toString()
157 100
158 const formData = new FormData() 101 const formData = new FormData()
159 formData.append('name', name) 102 formData.append('name', name)
160 formData.append('privacy', privacy.toString()) 103 // Put the video "private" -> we wait he validates the second step
161 formData.append('category', '' + category) 104 formData.append('privacy', VideoPrivacy.PRIVATE.toString())
162 formData.append('nsfw', '' + nsfw) 105 formData.append('nsfw', '' + nsfw)
163 formData.append('licence', '' + licence)
164 formData.append('channelId', '' + channelId) 106 formData.append('channelId', '' + channelId)
165 formData.append('videofile', videofile) 107 formData.append('videofile', videofile)
166 108
167 // Language is optional 109 this.isUploadingVideo = true
168 if (language) { 110 this.form.patchValue({
169 formData.append('language', '' + language) 111 name,
170 } 112 privacy,
171 113 nsfw,
172 formData.append('description', description) 114 channelId
173 115 })
174 for (let i = 0; i < tags.length; i++) {
175 formData.append(`tags[${i}]`, tags[i])
176 }
177 116
178 this.videoService.uploadVideo(formData).subscribe( 117 this.videoService.uploadVideo(formData).subscribe(
179 event => { 118 event => {
180 if (event.type === HttpEventType.UploadProgress) { 119 if (event.type === HttpEventType.UploadProgress) {
181 this.progressPercent = Math.round(100 * event.loaded / event.total) 120 this.videoUploadPercents = Math.round(100 * event.loaded / event.total)
182 } else if (event instanceof HttpResponse) { 121 } else if (event instanceof HttpResponse) {
183 console.log('Video uploaded.') 122 console.log('Video uploaded.')
184 this.notificationsService.success('Success', 'Video uploaded.')
185 123
186 // Display all the videos once it's finished 124 this.videoUploaded = true
187 this.router.navigate([ '/videos/list' ]) 125
126 this.videoUploadedId = event.body.video.id
188 } 127 }
189 }, 128 },
190 129
191 err => { 130 err => {
192 // Reset progress 131 // Reset progress
193 this.progressPercent = 0 132 this.videoUploadPercents = 0
194 this.error = err.message 133 this.error = err.message
195 } 134 }
196 ) 135 )
197 } 136 }
137
138 updateSecondStep () {
139 if (this.checkForm() === false) {
140 return
141 }
142
143 const video = new VideoEdit()
144 video.patch(this.form.value)
145 video.channel = this.firstStepChannelId
146 video.id = this.videoUploadedId
147
148 this.videoService.updateVideo(video)
149 .subscribe(
150 () => {
151 this.notificationsService.success('Success', 'Video published.')
152 this.router.navigate([ '/videos/watch', video.id ])
153 },
154
155 err => {
156 this.error = 'Cannot update the video.'
157 console.error(err)
158 }
159 )
160
161 }
198} 162}
diff --git a/client/src/app/videos/+video-edit/video-add.module.ts b/client/src/app/videos/+video-edit/video-add.module.ts
index f58d12dac..1efecdf4d 100644
--- a/client/src/app/videos/+video-edit/video-add.module.ts
+++ b/client/src/app/videos/+video-edit/video-add.module.ts
@@ -1,4 +1,5 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { ProgressBarModule } from 'primeng/primeng'
2import { SharedModule } from '../../shared' 3import { SharedModule } from '../../shared'
3import { VideoEditModule } from './shared/video-edit.module' 4import { VideoEditModule } from './shared/video-edit.module'
4import { VideoAddRoutingModule } from './video-add-routing.module' 5import { VideoAddRoutingModule } from './video-add-routing.module'
@@ -8,7 +9,8 @@ import { VideoAddComponent } from './video-add.component'
8 imports: [ 9 imports: [
9 VideoAddRoutingModule, 10 VideoAddRoutingModule,
10 VideoEditModule, 11 VideoEditModule,
11 SharedModule 12 SharedModule,
13 ProgressBarModule
12 ], 14 ],
13 15
14 declarations: [ 16 declarations: [
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 b9c6139b2..261b8a130 100644
--- a/client/src/app/videos/+video-edit/video-update.component.html
+++ b/client/src/app/videos/+video-edit/video-update.component.html
@@ -1,101 +1,20 @@
1<div class="row"> 1<div class="margin-content">
2 <div class="content-padding"> 2 <div class="title-page title-page-single">
3 3 Update {{ video?.name }}
4 <h3>Update {{ video?.name }}</h3> 4 </div>
5
6 <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
7 5
8 <form novalidate [formGroup]="form"> 6 <form novalidate [formGroup]="form">
9 <div class="form-group">
10 <label for="name">Name</label>
11 <input
12 type="text" class="form-control" id="name"
13 formControlName="name"
14 >
15 <div *ngIf="formErrors.name" class="alert alert-danger">
16 {{ formErrors.name }}
17 </div>
18 </div>
19
20 <div class="form-group">
21 <label for="privacy">Privacy</label>
22 <select class="form-control" id="privacy" formControlName="privacy">
23 <option></option>
24 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
25 </select>
26
27 <div *ngIf="formErrors.privacy" class="alert alert-danger">
28 {{ formErrors.privacy }}
29 </div>
30 </div>
31
32 <div class="form-group">
33 <input
34 type="checkbox" id="nsfw"
35 formControlName="nsfw"
36 >
37 <label for="nsfw">This video contains mature or explicit content</label>
38 </div>
39
40 <div class="form-group">
41 <label for="category">Category</label>
42 <select class="form-control" id="category" formControlName="category">
43 <option></option>
44 <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option>
45 </select>
46
47 <div *ngIf="formErrors.category" class="alert alert-danger">
48 {{ formErrors.category }}
49 </div>
50 </div>
51
52 <div class="form-group">
53 <label for="licence">Licence</label>
54 <select class="form-control" id="licence" formControlName="licence">
55 <option></option>
56 <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option>
57 </select>
58
59 <div *ngIf="formErrors.licence" class="alert alert-danger">
60 {{ formErrors.licence }}
61 </div>
62 </div>
63
64 <div class="form-group">
65 <label for="language">Language</label>
66 <select class="form-control" id="language" formControlName="language">
67 <option></option>
68 <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option>
69 </select>
70
71 <div *ngIf="formErrors.language" class="alert alert-danger">
72 {{ formErrors.language }}
73 </div>
74 </div>
75
76 <div class="form-group">
77 <label class="label-tags">Tags</label> <span class="little-information">(press enter to add the tag)</span>
78 <tag-input
79 [ngModel]="tags" [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
80 formControlName="tags" maxItems="5" modelAsStrings="true"
81 ></tag-input>
82 </div>
83 7
84 <div class="form-group"> 8 <my-video-edit
85 <label for="description">Description</label> 9 [form]="form" [formErrors]="formErrors"
86 <my-video-description formControlName="description"></my-video-description> 10 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies"
11 ></my-video-edit>
87 12
88 <div *ngIf="formErrors.description" class="alert alert-danger"> 13 <div class="submit-container">
89 {{ formErrors.description }} 14 <div class="submit-button" (click)="update()" [ngClass]="{ disabled: !form.valid }">
15 <span class="icon icon-validate"></span>
16 <input type="button" value="Update" />
90 </div> 17 </div>
91 </div> 18 </div>
92
93 <div class="form-group">
94 <input
95 type="button" value="Update" class="btn btn-default form-control"
96 (click)="update()"
97 >
98 </div>
99 </form> 19 </form>
100 </div>
101</div> 20</div>
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 0e966cb50..d1da8b6d8 100644
--- a/client/src/app/videos/+video-edit/video-update.component.ts
+++ b/client/src/app/videos/+video-edit/video-update.component.ts
@@ -1,23 +1,14 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { FormBuilder, FormGroup } from '@angular/forms' 2import { FormBuilder, FormGroup } from '@angular/forms'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import 'rxjs/add/observable/forkJoin'
5
6import { NotificationsService } from 'angular2-notifications' 4import { NotificationsService } from 'angular2-notifications'
7 5import 'rxjs/add/observable/forkJoin'
8import { ServerService } from '../../core'
9import {
10 FormReactive,
11 VIDEO_NAME,
12 VIDEO_CATEGORY,
13 VIDEO_LICENCE,
14 VIDEO_LANGUAGE,
15 VIDEO_DESCRIPTION,
16 VIDEO_TAGS,
17 VIDEO_PRIVACY
18} from '../../shared'
19import { VideoEdit, VideoService } from '../shared'
20import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum' 6import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum'
7import { ServerService } from '../../core'
8import { FormReactive } from '../../shared'
9import { ValidatorMessage } from '../../shared/forms/form-validators'
10import { VideoEdit } from '../../shared/video/video-edit.model'
11import { VideoService } from '../../shared/video/video.service'
21 12
22@Component({ 13@Component({
23 selector: 'my-videos-update', 14 selector: 'my-videos-update',
@@ -26,34 +17,13 @@ import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.
26}) 17})
27 18
28export class VideoUpdateComponent extends FormReactive implements OnInit { 19export class VideoUpdateComponent extends FormReactive implements OnInit {
29 tags: string[] = []
30 videoCategories = []
31 videoLicences = []
32 videoLanguages = []
33 videoPrivacies = []
34 video: VideoEdit 20 video: VideoEdit
35 21
36 tagValidators = VIDEO_TAGS.VALIDATORS
37 tagValidatorsMessages = VIDEO_TAGS.MESSAGES
38
39 error: string = null 22 error: string = null
40 form: FormGroup 23 form: FormGroup
41 formErrors = { 24 formErrors: { [ id: string ]: string } = {}
42 name: '', 25 validationMessages: ValidatorMessage = {}
43 privacy: '', 26 videoPrivacies = []
44 category: '',
45 licence: '',
46 language: '',
47 description: ''
48 }
49 validationMessages = {
50 name: VIDEO_NAME.MESSAGES,
51 privacy: VIDEO_PRIVACY.MESSAGES,
52 category: VIDEO_CATEGORY.MESSAGES,
53 licence: VIDEO_LICENCE.MESSAGES,
54 language: VIDEO_LANGUAGE.MESSAGES,
55 description: VIDEO_DESCRIPTION.MESSAGES
56 }
57 27
58 fileError = '' 28 fileError = ''
59 29
@@ -69,30 +39,16 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
69 } 39 }
70 40
71 buildForm () { 41 buildForm () {
72 this.form = this.formBuilder.group({ 42 this.form = this.formBuilder.group({})
73 name: [ '', VIDEO_NAME.VALIDATORS ],
74 privacy: [ '', VIDEO_PRIVACY.VALIDATORS ],
75 nsfw: [ false ],
76 category: [ '', VIDEO_CATEGORY.VALIDATORS ],
77 licence: [ '', VIDEO_LICENCE.VALIDATORS ],
78 language: [ '', VIDEO_LANGUAGE.VALIDATORS ],
79 description: [ '', VIDEO_DESCRIPTION.VALIDATORS ],
80 tags: [ '' ]
81 })
82
83 this.form.valueChanges.subscribe(data => this.onValueChanged(data)) 43 this.form.valueChanges.subscribe(data => this.onValueChanged(data))
84 } 44 }
85 45
86 ngOnInit () { 46 ngOnInit () {
87 this.buildForm() 47 this.buildForm()
88 48
89 this.videoCategories = this.serverService.getVideoCategories()
90 this.videoLicences = this.serverService.getVideoLicences()
91 this.videoLanguages = this.serverService.getVideoLanguages()
92 this.videoPrivacies = this.serverService.getVideoPrivacies() 49 this.videoPrivacies = this.serverService.getVideoPrivacies()
93 50
94 const uuid: string = this.route.snapshot.params['uuid'] 51 const uuid: string = this.route.snapshot.params['uuid']
95
96 this.videoService.getVideo(uuid) 52 this.videoService.getVideo(uuid)
97 .switchMap(video => { 53 .switchMap(video => {
98 return this.videoService 54 return this.videoService
@@ -104,7 +60,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
104 video => { 60 video => {
105 this.video = new VideoEdit(video) 61 this.video = new VideoEdit(video)
106 62
107 // We cannot set private a video that was not private anymore 63 // We cannot set private a video that was not private
108 if (video.privacy !== VideoPrivacy.PRIVATE) { 64 if (video.privacy !== VideoPrivacy.PRIVATE) {
109 const newVideoPrivacies = [] 65 const newVideoPrivacies = []
110 for (const p of this.videoPrivacies) { 66 for (const p of this.videoPrivacies) {
diff --git a/client/src/app/videos/+video-watch/video-download.component.html b/client/src/app/videos/+video-watch/video-download.component.html
index ddc57e999..7efc79e93 100644
--- a/client/src/app/videos/+video-watch/video-download.component.html
+++ b/client/src/app/videos/+video-watch/video-download.component.html
@@ -6,18 +6,19 @@
6 <button type="button" class="close" aria-label="Close" (click)="hide()"> 6 <button type="button" class="close" aria-label="Close" (click)="hide()">
7 <span aria-hidden="true">&times;</span> 7 <span aria-hidden="true">&times;</span>
8 </button> 8 </button>
9 <h4 class="modal-title">Download</h4> 9 <h4 class="title-page title-page-single">Download</h4>
10 </div> 10 </div>
11 11
12 <div class="modal-body"> 12 <div class="modal-body">
13 <div *ngFor="let file of video.files" class="resolution-block"> 13 <div *ngFor="let file of video.files" class="resolution-block">
14 <label>{{ file.resolutionLabel }}</label> 14 <label>{{ file.resolutionLabel }}</label>
15 <a class="btn btn-default " target="_blank" [href]="file.torrentUrl"> 15
16 <span class="glyphicon glyphicon-download"></span> 16 <a class="orange-button-link " target="_blank" [href]="file.torrentUrl">
17 <span class="icon icon-download"></span>
17 Torrent file 18 Torrent file
18 </a> 19 </a>
19 <a class="btn btn-default" target="_blank" [href]="file.fileUrl"> 20 <a class="orange-button-link" target="_blank" [href]="file.fileUrl">
20 <span class="glyphicon glyphicon-download"></span> 21 <span class="icon icon-download"></span>
21 Download 22 Download
22 </a> 23 </a>
23 24
diff --git a/client/src/app/videos/+video-watch/video-download.component.scss b/client/src/app/videos/+video-watch/video-download.component.scss
new file mode 100644
index 000000000..c9d5af9c1
--- /dev/null
+++ b/client/src/app/videos/+video-watch/video-download.component.scss
@@ -0,0 +1,23 @@
1.resolution-block:not(:first-child) {
2 margin-top: 30px;
3}
4
5.orange-button-link {
6 margin-right: 10px;
7}
8
9label {
10 display: block;
11}
12
13.icon {
14 @include icon(21px);
15
16 margin-right: 5px;
17 position: relative;
18 top: -1px;
19
20 &.icon-download {
21 background-image: url('../../../assets/images/video/download-white.svg');
22 }
23}
diff --git a/client/src/app/videos/+video-watch/video-download.component.ts b/client/src/app/videos/+video-watch/video-download.component.ts
index c32f8d586..095df1698 100644
--- a/client/src/app/videos/+video-watch/video-download.component.ts
+++ b/client/src/app/videos/+video-watch/video-download.component.ts
@@ -1,13 +1,11 @@
1import { Component, Input, ViewChild } from '@angular/core' 1import { Component, Input, ViewChild } from '@angular/core'
2
3import { ModalDirective } from 'ngx-bootstrap/modal' 2import { ModalDirective } from 'ngx-bootstrap/modal'
4 3import { VideoDetails } from '../../shared/video/video-details.model'
5import { VideoDetails } from '../shared'
6 4
7@Component({ 5@Component({
8 selector: 'my-video-download', 6 selector: 'my-video-download',
9 templateUrl: './video-download.component.html', 7 templateUrl: './video-download.component.html',
10 styles: [ '.resolution-block { margin-top: 20px; }' ] 8 styleUrls: [ './video-download.component.scss' ]
11}) 9})
12export class VideoDownloadComponent { 10export class VideoDownloadComponent {
13 @Input() video: VideoDetails = null 11 @Input() video: VideoDetails = null
diff --git a/client/src/app/videos/+video-watch/video-report.component.html b/client/src/app/videos/+video-watch/video-report.component.html
index ceb7cf50a..20474bab4 100644
--- a/client/src/app/videos/+video-watch/video-report.component.html
+++ b/client/src/app/videos/+video-watch/video-report.component.html
@@ -6,28 +6,28 @@
6 <button type="button" class="close" aria-label="Close" (click)="hide()"> 6 <button type="button" class="close" aria-label="Close" (click)="hide()">
7 <span aria-hidden="true">&times;</span> 7 <span aria-hidden="true">&times;</span>
8 </button> 8 </button>
9 <h4 class="modal-title">Report video</h4> 9 <h4 class="title-page title-page-single">Report video</h4>
10 </div> 10 </div>
11 11
12 <div class="modal-body"> 12 <div class="modal-body">
13 13
14 <form novalidate [formGroup]="form"> 14 <form novalidate [formGroup]="form" (ngSubmit)="report()">
15 <div class="form-group"> 15 <div class="form-group">
16 <label for="reason">Reason</label> 16 <label for="reason">Reason</label>
17 <textarea 17 <textarea
18 id="reason" class="form-control" placeholder="Reason..." 18 id="reason" class="form-control" placeholder="Reason..."
19 formControlName="reason" 19 formControlName="reason" [ngClass]="{ 'input-error': formErrors['reason'] }"
20 > 20 >
21 </textarea> 21 </textarea>
22 <div *ngIf="formErrors.reason" class="alert alert-danger"> 22 <div *ngIf="formErrors.reason" class="form-error">
23 {{ formErrors.reason }} 23 {{ formErrors.reason }}
24 </div> 24 </div>
25 </div> 25 </div>
26 26
27 <div class="form-group"> 27 <div class="form-group">
28 <input 28 <input
29 type="button" value="Report" class="btn btn-default form-control" 29 type="submit" value="Report" class="orange-button"
30 [disabled]="!form.valid" (click)="report()" 30 [disabled]="!form.valid"
31 > 31 >
32 </div> 32 </div>
33 </form> 33 </form>
diff --git a/client/src/app/videos/+video-watch/video-report.component.ts b/client/src/app/videos/+video-watch/video-report.component.ts
index fc9b5a9d4..b94e4144e 100644
--- a/client/src/app/videos/+video-watch/video-report.component.ts
+++ b/client/src/app/videos/+video-watch/video-report.component.ts
@@ -1,11 +1,9 @@
1import { Component, Input, OnInit, ViewChild } from '@angular/core' 1import { Component, Input, OnInit, ViewChild } from '@angular/core'
2import { FormBuilder, FormGroup } from '@angular/forms' 2import { FormBuilder, FormGroup } from '@angular/forms'
3
4import { ModalDirective } from 'ngx-bootstrap/modal'
5import { NotificationsService } from 'angular2-notifications' 3import { NotificationsService } from 'angular2-notifications'
6 4import { ModalDirective } from 'ngx-bootstrap/modal'
7import { FormReactive, VideoAbuseService, VIDEO_ABUSE_REASON } from '../../shared' 5import { FormReactive, VIDEO_ABUSE_REASON, VideoAbuseService } from '../../shared'
8import { VideoDetails, VideoService } from '../shared' 6import { VideoDetails } from '../../shared/video/video-details.model'
9 7
10@Component({ 8@Component({
11 selector: 'my-video-report', 9 selector: 'my-video-report',
diff --git a/client/src/app/videos/+video-watch/video-share.component.html b/client/src/app/videos/+video-watch/video-share.component.html
index 88f59c063..36ec38d88 100644
--- a/client/src/app/videos/+video-watch/video-share.component.html
+++ b/client/src/app/videos/+video-watch/video-share.component.html
@@ -6,7 +6,7 @@
6 <button type="button" class="close" aria-label="Close" (click)="hide()"> 6 <button type="button" class="close" aria-label="Close" (click)="hide()">
7 <span aria-hidden="true">&times;</span> 7 <span aria-hidden="true">&times;</span>
8 </button> 8 </button>
9 <h4 class="modal-title">Share</h4> 9 <h4 class="title-page title-page-single">Share</h4>
10 </div> 10 </div>
11 11
12 <div class="modal-body"> 12 <div class="modal-body">
diff --git a/client/src/app/videos/+video-watch/video-share.component.ts b/client/src/app/videos/+video-watch/video-share.component.ts
index aeef65ecf..4df9adf29 100644
--- a/client/src/app/videos/+video-watch/video-share.component.ts
+++ b/client/src/app/videos/+video-watch/video-share.component.ts
@@ -1,8 +1,6 @@
1import { Component, Input, ViewChild } from '@angular/core' 1import { Component, Input, ViewChild } from '@angular/core'
2
3import { ModalDirective } from 'ngx-bootstrap/modal' 2import { ModalDirective } from 'ngx-bootstrap/modal'
4 3import { VideoDetails } from '../../shared/video/video-details.model'
5import { VideoDetails } from '../shared'
6 4
7@Component({ 5@Component({
8 selector: 'my-video-share', 6 selector: 'my-video-share',
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 f528d73c3..f99e84caf 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.html
+++ b/client/src/app/videos/+video-watch/video-watch.component.html
@@ -1,197 +1,153 @@
1<div *ngIf="error" class="row">
2 <div class="alert alert-danger">
3 The video load seems to be abnormally long.
4 <ul>
5 <li>Maybe the server {{ video.serverHost }} is down :(</li>
6 <li>
7 If not, you can report an issue on
8 <a href="https://github.com/Chocobozzz/PeerTube/issues" title="Report an issue">
9 https://github.com/Chocobozzz/PeerTube/issues
10 </a>
11 </li>
12 </ul>
13 </div>
14</div>
15
16<div class="row"> 1<div class="row">
17 <!-- We need the video container for videojs so we just hide it --> 2 <!-- We need the video container for videojs so we just hide it -->
18 <div [hidden]="videoNotFound" class="embed-responsive embed-responsive-19by9"> 3 <div [hidden]="videoNotFound" id="video-container">
19 <video id="video-container" class="video-js vjs-sublime-skin"></video> 4 <video id="video-element" class="video-js vjs-peertube-skin vjs-fluid"></video>
20 </div> 5 </div>
21 6
22 <div *ngIf="videoNotFound" id="video-not-found">Video not found :'(</div> 7 <div *ngIf="videoNotFound" id="video-not-found">Video not found :'(</div>
23</div>
24
25<!-- P2P information -->
26<div id="torrent-info" class="row">
27 <div id="torrent-info-download" class="col-md-4 col-sm-4 col-xs-4">Download: {{ downloadSpeed | bytes }}/s</div>
28 <div id="torrent-info-upload" class="col-md-4 col-sm-4 col-xs-4">Upload: {{ uploadSpeed | bytes }}/s</div>
29 <div id="torrent-info-peers" class="col-md-4 col-sm-4 col-xs-4">Number of peers: {{ numPeers }}</div>
30</div>
31
32<!-- Video information -->
33<div *ngIf="video !== null" id="video-info">
34 <div class="row video-name-views">
35 <div class="col-xs-8 col-md-8 video-name">
36 {{ video.name }}
37 </div>
38
39 <div class="col-xs-4 col-md-4 pull-right video-views">
40 {{ video.views}} views
41 </div>
42 </div>
43
44 <div class="row video-small-blocks">
45 <div class="col-xs-5 col-xs-3 col-md-3 video-small-block video-small-block-account">
46 <a class="option" title="Access to all videos of this user" [routerLink]="['/videos/list', { field: 'account', search: video.account }]">
47 <span class="glyphicon glyphicon-user"></span>
48 <span class="video-small-block-text">{{ video.by }}</span>
49 </a>
50 </div>
51
52 <div class="col-xs-2 col-md-3 video-small-block video-small-block-share">
53 <a class="option" (click)="showShareModal()" title="Share the video">
54 <span class="glyphicon glyphicon-share"></span>
55 <span class="hidden-xs video-small-block-text">Share</span>
56 </a>
57 </div>
58 8
59 <div class="col-xs-2 col-md-3 video-small-block video-small-block-more"> 9 <!-- Video information -->
60 <div class="video-small-block-dropdown" dropdown dropup="true" placement="right"> 10 <div *ngIf="video" class="margin-content video-bottom">
61 <a class="option" title="Access to more options" dropdownToggle> 11 <div class="video-info">
62 <span class="glyphicon glyphicon-option-horizontal"></span> 12 <div class="video-info-name-actions">
63 <span class="hidden-xs video-small-block-text">More</span> 13 <div class="video-info-name">{{ video.name }}</div>
64 </a> 14
65 15 <div class="video-info-actions">
66 <ul *dropdownMenu class="dropdown-menu" id="more-menu" role="menu" aria-labelledby="single-button"> 16 <div *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'like' }" class="action-button">
67 <li *ngIf="canUserUpdateVideo()" role="menuitem"> 17 <span class="icon icon-like" title="Like this video" (click)="setLike()"></span>
68 <a class="dropdown-item" title="Update this video" href="#" [routerLink]="[ '/videos/edit', video.uuid ]"> 18 </div>
69 <span class="glyphicon glyphicon-pencil"></span> Update 19
70 </a> 20 <div *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'dislike' }" class="action-button">
71 </li> 21 <span class="icon icon-dislike" title="Dislike this video" (click)="setDislike()"></span>
72 22 </div>
73 <li role="menuitem"> 23
74 <a class="dropdown-item" title="Download the video" href="#" (click)="showDownloadModal($event)"> 24 <div (click)="showShareModal()" class="action-button">
75 <span class="glyphicon glyphicon-download-alt"></span> Download 25 <span class="icon icon-share"></span>
76 </a> 26 Share
77 </li> 27 </div>
78 28
79 <li *ngIf="isUserLoggedIn()" role="menuitem"> 29 <div class="action-more" dropdown dropup="true" placement="right">
80 <a class="dropdown-item" title="Report this video" href="#" (click)="showReportModal($event)"> 30 <div class="action-button" dropdownToggle>
81 <span class="glyphicon glyphicon-alert"></span> Report 31 <span class="icon icon-more"></span>
82 </a> 32 </div>
83 </li> 33
84 34 <ul *dropdownMenu class="dropdown-menu" id="more-menu" role="menu" aria-labelledby="single-button">
85 <li *ngIf="isVideoRemovable()" role="menuitem"> 35 <li role="menuitem">
86 <a class="dropdown-item" title="Delete this video" href="#" (click)="removeVideo($event)"> 36 <a class="dropdown-item" title="Download the video" href="#" (click)="showDownloadModal($event)">
87 <span class="glyphicon glyphicon-remove"></span> Delete 37 <span class="icon icon-download"></span> Download
88 </a> 38 </a>
89 </li> 39 </li>
90 40
91 <li *ngIf="isVideoBlacklistable()" role="menuitem"> 41 <li *ngIf="isUserLoggedIn()" role="menuitem">
92 <a class="dropdown-item" title="Blacklist this video" href="#" (click)="blacklistVideo($event)"> 42 <a class="dropdown-item" title="Report this video" href="#" (click)="showReportModal($event)">
93 <span class="glyphicon glyphicon-eye-close"></span> Blacklist 43 <span class="icon icon-alert"></span> Report
94 </a> 44 </a>
95 </li> 45 </li>
96 </ul> 46
47 <li *ngIf="isVideoBlacklistable()" role="menuitem">
48 <a class="dropdown-item" title="Blacklist this video" href="#" (click)="blacklistVideo($event)">
49 <span class="icon icon-blacklist"></span> Blacklist
50 </a>
51 </li>
52 </ul>
53 </div>
54 </div>
97 </div> 55 </div>
98 </div>
99 56
100 <div class="col-xs-3 col-md-3 video-small-block video-small-block-rating"> 57 <div class="video-info-date-views-bar">
101 <div class="video-small-block-like"> 58 <div class="video-info-date-views">
102 <span 59 {{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views
103 class="glyphicon glyphicon-thumbs-up" title="Like this video" 60 </div>
104 [ngClass]="{ 'interactive': isUserLoggedIn(), 'activated': userRating === 'like' }" (click)="setLike()"
105 ></span>
106 61
107 <span class="video-small-block-text"> 62 <div *ngIf="video.likes !== 0 || video.dislikes !== 0" class="video-info-likes-dislikes-bar">
108 {{ video.likes }} 63 <div class="likes-bar" [ngStyle]="{ 'width.%': video.likesPercent }"></div>
109 </span> 64 </div>
110 </div> 65 </div>
111 66
112 <div class="video-small-block-dislike"> 67 <div class="video-info-channel">
113 <span 68 {{ video.channel.name }}
114 class="glyphicon glyphicon-thumbs-down" title="Dislike this video" 69 <!-- Here will be the subscribe button -->
115 [ngClass]="{ 'interactive': isUserLoggedIn(), 'activated': userRating === 'dislike' }" (click)="setDislike()"
116 ></span>
117
118 <span class="video-small-block-text">
119 {{ video.dislikes }}
120 </span>
121 </div> 70 </div>
122 </div>
123 </div>
124 71
125 <div class="row video-details"> 72 <div class="video-info-by">
126 <div class="video-details-date-description col-xs-8 col-md-9"> 73 By {{ video.by }}
127 <div class="video-details-date"> 74 <img [src]="getAvatarPath()" alt="Account avatar" />
128 Published on {{ video.createdAt | date:'short' }}
129 </div> 75 </div>
130 76
131 <div class="video-details-description" [innerHTML]="videoHTMLDescription"></div> 77 <div class="video-info-description">
78 <div class="video-info-description-html" [innerHTML]="videoHTMLDescription"></div>
132 79
133 <div class="video-details-description-more" *ngIf="completeDescriptionShown === false && video.description.length === 250" (click)="showMoreDescription()"> 80 <div class="video-info-description-more" *ngIf="completeDescriptionShown === false && video.description?.length === 250" (click)="showMoreDescription()">
134 Show more 81 Show more
135 <span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-down"></span> 82 <span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-down"></span>
136 <my-loader class="description-loading" [loading]="descriptionLoading"></my-loader> 83 <my-loader class="description-loading" [loading]="descriptionLoading"></my-loader>
137 </div> 84 </div>
138 85
139 <div *ngIf="completeDescriptionShown === true" (click)="showLessDescription()" class="video-details-description-more"> 86 <div *ngIf="completeDescriptionShown === true" (click)="showLessDescription()" class="video-info-description-more">
140 Show less 87 Show less
141 <span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-up"></span> 88 <span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-up"></span>
89 </div>
142 </div> 90 </div>
143 </div>
144 91
145 <div class="video-details-attributes col-xs-4 col-md-3"> 92 <div class="video-attributes">
146 <div class="video-details-attribute"> 93 <div class="video-attribute">
147 <span class="video-details-attribute-label"> 94 <span class="video-attribute-label">
148 Privacy: 95 Privacy
149 </span> 96 </span>
150 <span class="video-details-attribute-value"> 97 <span class="video-attribute-value">
151 {{ video.privacyLabel }} 98 {{ video.privacyLabel }}
152 </span> 99 </span>
153 </div> 100 </div>
154 101
155 <div class="video-details-attribute"> 102 <div class="video-attribute">
156 <span class="video-details-attribute-label"> 103 <span class="video-attribute-label">
157 Category: 104 Category
158 </span> 105 </span>
159 <span class="video-details-attribute-value"> 106 <span class="video-attribute-value">
160 {{ video.categoryLabel }} 107 {{ video.categoryLabel }}
161 </span> 108 </span>
162 </div> 109 </div>
163 110
164 <div class="video-details-attribute"> 111 <div class="video-attribute">
165 <span class="video-details-attribute-label"> 112 <span class="video-attribute-label">
166 Licence: 113 Licence
167 </span> 114 </span>
168 <span class="video-details-attribute-value"> 115 <span class="video-attribute-value">
169 {{ video.licenceLabel }} 116 {{ video.licenceLabel }}
170 </span> 117 </span>
171 </div> 118 </div>
172 119
173 <div class="video-details-attribute"> 120 <div class="video-attribute">
174 <span class="video-details-attribute-label"> 121 <span class="video-attribute-label">
175 Language: 122 Language
176 </span> 123 </span>
177 <span class="video-details-attribute-value"> 124 <span class="video-attribute-value">
178 {{ video.languageLabel }} 125 {{ video.languageLabel }}
179 </span> 126 </span>
180 </div> 127 </div>
181 128
182 <div class="video-details-attribute"> 129 <div class="video-attribute">
183 <span class="video-details-attribute-label"> 130 <span class="video-attribute-label">
184 Tags: 131 Tags
185 </span> 132 </span>
186 133
187 <div class="video-details-tags"> 134 <span class="video-attribute-value">
188 <a *ngFor="let tag of video.tags" [routerLink]="['/videos/list', { field: 'tags', search: tag }]" class="label label-primary"> 135 {{ getVideoTags() }}
189 {{ tag }} 136 </span>
190 </a>
191 </div> 137 </div>
192 </div> 138 </div>
193 139
194 </div> 140 </div>
141
142 <div class="other-videos">
143 <div class="title-page title-page-single">
144 Other videos
145 </div>
146
147 <div *ngFor="let video of otherVideos">
148 <my-video-miniature [video]="video" [user]="user"></my-video-miniature>
149 </div>
150 </div>
195 </div> 151 </div>
196</div> 152</div>
197 153
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 cad21dd18..9daa757b4 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.scss
+++ b/client/src/app/videos/+video-watch/video-watch.component.scss
@@ -1,6 +1,22 @@
1#video-container { 1#video-container {
2 width: 100%; 2 background-color: #000;
3 height: 100%; 3 display: flex;
4 justify-content: center;
5
6 #video-element {
7 width: 888px;
8 height: 500px;
9
10 @media screen and (max-width: 800px) {
11 height: auto;
12 }
13
14 // VideoJS create an inner video player
15 video {
16 outline: 0;
17 position: relative !important;
18 }
19 }
4} 20}
5 21
6#video-not-found { 22#video-not-found {
@@ -11,175 +27,153 @@
11 font-weight: bold; 27 font-weight: bold;
12} 28}
13 29
14.embed-responsive { 30.video-bottom {
15 height: 500px; 31 margin-top: 40px;
32 display: flex;
16 33
17 @media screen and (max-width: 600px) { 34 .video-info {
18 height: 300px; 35 flex-grow: 1;
19 } 36 margin-right: 28px;
20}
21 37
22#torrent-info { 38 .video-info-name-actions {
23 font-size: 10px; 39 display: flex;
24 margin-top: 10px; 40 align-items: center;
25 text-align: center;
26
27 div {
28 min-width: 60px;
29 }
30}
31
32#video-info {
33 .video-name-views {
34 font-weight: bold;
35 font-size: 18px;
36 min-height: $video-watch-title-height;
37 display: flex;
38 align-items: center;
39
40 .video-name {
41 padding-left: $video-watch-info-padding-left;
42 }
43 41
44 .video-views { 42 .video-info-name {
45 text-align: right; 43 font-size: 27px;
46 // Keep a symmetry with the video name 44 font-weight: $font-semibold;
47 padding-right: $video-watch-info-padding-left 45 flex-grow: 1;
48 } 46 }
49 47
50 } 48 .video-info-actions {
49 .action-button {
50 @include peertube-button;
51 @include grey-button;
51 52
52 .video-small-blocks { 53 font-size: 15px;
53 height: $video-watch-info-height; 54 font-weight: $font-semibold;
54 color: $video-watch-info-color; 55 display: inline-block;
55 border-color: $video-watch-border-color; 56 padding: 0 10px 0 10px;
56 border-width: 1px 0px;
57 border-style: solid;
58 57
59 .video-small-block { 58 .icon {
60 height: $video-watch-info-height; 59 @include icon(21px);
61 display: flex;
62 flex-direction: column;
63 justify-content: center;
64 text-align: center;
65 60
66 a { 61 position: relative;
67 cursor: pointer; 62 top: -2px;
68 transition: color 0.3s;
69 white-space: nowrap;
70 overflow: hidden;
71 text-overflow: ellipsis;
72
73 &, &:hover {
74 color: inherit;
75 text-decoration:none;
76 }
77 63
78 &:hover { 64 &.icon-like {
79 color: #000 !important; 65 background-image: url('../../../assets/images/video/like-grey.svg');
80 } 66 }
81 67
82 &:hover > .glyphicon { 68 &.icon-dislike {
83 opacity: 1 !important; 69 background-image: url('../../../assets/images/video/dislike-grey.svg');
84 } 70 }
85 }
86 71
87 .option .glyphicon { 72 &.icon-share {
88 font-size: 22px; 73 background-image: url('../../../assets/images/video/share.svg');
89 color: inherit; 74 }
90 opacity: 0.15;
91 margin-bottom: 10px;
92 transition: opacity 0.3s;
93 }
94 75
95 .video-small-block-text { 76 &.icon-more {
96 font-size: 15px; 77 background-image: url('../../../assets/images/video/more.svg');
97 font-weight: bold; 78 top: -1px;
98 } 79 }
99 } 80 }
100 81
101 .video-small-block:not(:last-child) { 82 &.activated {
102 border-width: 0 1px 0 0; 83 @include orange-button;
103 border-color: $video-watch-border-color;
104 border-style: solid;
105 }
106 84
107 .video-small-block-account, .video-small-block-more { 85 .icon-like {
108 a.option { 86 background-image: url('../../../assets/images/video/like-white.svg');
109 display: block; 87 }
110 88
111 .glyphicon { 89 .icon-dislike {
112 display: block; 90 background-image: url('../../../assets/images/video/dislike-white.svg');
91 }
92 }
113 } 93 }
114 }
115 }
116 94
117 .video-small-block-share, .video-small-block-more { 95 .action-more {
118 a.option { 96 display: inline-block;
119 display: block; 97
120 98 .dropdown-menu .icon {
121 .glyphicon { 99 display: inline-block;
122 display: block; 100 background-repeat: no-repeat;
101 background-size: contain;
102 width: 21px;
103 height: 21px;
104 vertical-align: middle;
105 margin-right: 5px;
106 position: relative;
107 top: -1px;
108
109 &.icon-download {
110 background-image: url('../../../assets/images/video/download-grey.svg');
111 }
112
113 &.icon-alert {
114 background-image: url('../../../assets/images/video/alert.svg');
115 }
116
117 &.icon-blacklist {
118 background-image: url('../../../assets/images/video/eye-closed.svg');
119 }
120 }
123 } 121 }
124 } 122 }
125 } 123 }
126 124
127 .video-small-block-more .video-small-block-dropdown { 125 .video-info-date-views-bar {
128 position: relative; 126 display: flex;
129
130 .dropdown-item .glyphicon {
131 margin-right: 5px;
132 }
133 }
134
135 .video-small-block-rating {
136 127
137 .video-small-block-like { 128 .video-info-date-views {
129 font-size: 16px;
138 margin-bottom: 10px; 130 margin-bottom: 10px;
131 flex-grow: 1;
139 } 132 }
140 133
141 .video-small-block-text { 134 .video-info-likes-dislikes-bar {
142 vertical-align: top; 135 height: 5px;
143 } 136 width: 186px;
137 background-color: #E5E5E5;
138 margin-top: 25px;
144 139
145 .glyphicon { 140 .likes-bar {
146 font-size: 18px; 141 height: 100%;
147 margin: 0 10px 0 0; 142 background-color: #39CC0B;
148 opacity: 0.3; 143 }
149 } 144 }
145 }
150 146
151 .interactive { 147 .video-info-channel {
152 cursor: pointer; 148 font-weight: $font-semibold;
153 transition: opacity, color 0.3s; 149 font-size: 15px;
150 }
154 151
155 &.activated, &:hover { 152 .video-info-by {
156 opacity: 1; 153 display: flex;
157 color: #000; 154 align-items: center;
158 } 155 font-size: 13px;
156
157 img {
158 width: 16px;
159 height: 16px;
160 margin-left: 3px;
159 } 161 }
160 } 162 }
161 }
162 163
163 .video-details { 164 .video-info-description {
164 margin-top: 30px; 165 margin: 20px 0;
165 166 font-size: 15px;
166 .video-details-date-description {
167 padding-left: $video-watch-info-padding-left;
168 167
169 .description-loading { 168 .description-loading {
170 display: inline-block; 169 display: inline-block;
171 } 170 }
172 171
173 .video-details-date { 172 .video-info-description-more {
174 font-weight: bold;
175 margin-bottom: 30px;
176 }
177
178 .video-details-description-more {
179 cursor: pointer; 173 cursor: pointer;
180 margin-top: 15px; 174 font-weight: $font-semibold;
181 font-weight: bold; 175 color: #585858;
182 color: #acaeb7; 176 font-size: 14px;
183 177
184 .glyphicon { 178 .glyphicon {
185 position: relative; 179 position: relative;
@@ -188,109 +182,68 @@
188 } 182 }
189 } 183 }
190 184
191 .video-details-attributes { 185 .video-attributes {
192 font-weight: bold; 186 .video-attribute {
193 font-size: 12px; 187 font-size: 13px;
194 188 display: block;
195 .video-details-attribute { 189 margin-bottom: 12px;
196 display: flex;
197 190
198 .video-details-attribute-label { 191 .video-attribute-label {
199 color: $video-watch-info-color; 192 width: 86px;
200 flex-basis: 60px; 193 display: inline-block;
201 flex-grow: 0; 194 color: #585858;
202 flex-shrink: 0; 195 font-weight: $font-bold;
203 margin-right: 5px;
204 } 196 }
205 } 197 }
206 } 198 }
207
208 .video-details-tags {
209 display: flex;
210 flex-wrap: wrap;
211
212 a {
213 margin: 0 3px 3px 0;
214 font-size: 11px;
215 }
216 }
217 } 199 }
218 200
219 @media screen and (max-width: 800px) { 201 .other-videos {
220 .video-name-views { 202 .title-page {
221 .video-name { 203 margin-top: 0;
222 padding-left: 5px;
223 padding-right: 0px;
224 }
225
226 .video-views {
227 padding-left: 0px;
228 padding-right: 5px;
229 }
230 } 204 }
231 205
232 .video-small-blocks { 206 /deep/ .video-miniature {
233 a, .video-small-block-text { 207 display: flex;
234 font-size: 13px !important; 208 height: 100%;
235 } 209 margin-bottom: 20px;
236
237 .glyphicon {
238 font-size: 18px !important;
239 }
240 210
241 .video-small-block-account { 211 .video-miniature-information {
242 padding-left: 10px; 212 margin-left: 10px;
243 padding-right: 10px;
244 } 213 }
245 } 214 }
215 }
216}
246 217
247 .video-details {
248 .video-details-date-description {
249 padding-left: 10px;
250 font-size: 13px !important;
251 }
252
253 .video-details-attributes {
254 font-size: 11px !important;
255 218
256 .video-details-attribute-label { 219@media screen and (max-width: 1000px) {
257 width: 50px; 220 .other-videos {
258 } 221 display: none;
259 }
260 }
261 } 222 }
223}
262 224
263 @media screen and (max-width: 500px) { 225@media screen and (max-width: 800px) {
264 .video-name-views { 226 .video-bottom {
265 font-size: 16px !important; 227 margin: 20px 0 0 0;
266 }
267 228
268 // Keep the same hierarchy than max-width: 800px 229 .video-info {
269 .video-small-blocks { 230 margin-right: 0;
270 a, .video-small-block-text {
271 font-size: 10px !important;
272 }
273 231
274 .video-small-block-account { 232 .video-info-name-actions {
275 padding-left: 5px; 233 align-items: left;
276 padding-right: 5px; 234 flex-direction: column;
235 margin-bottom: 30px;
277 } 236 }
278 }
279 237
280 .video-details { 238 .video-info-date-views-bar {
281 .video-details-date-description { 239 align-items: left;
240 flex-direction: column;
282 margin-bottom: 30px; 241 margin-bottom: 30px;
283 width: 100%;
284 242
285 .video-details-date { 243 .video-info-likes-dislikes-bar {
286 margin-bottom: 15px; 244 margin-top: 0;
287 } 245 }
288 } 246 }
289
290 .video-details-attributes {
291 padding-left: 10px;
292 padding-right: 10px;
293 }
294 } 247 }
295 } 248 }
296} 249}
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 b26f3092f..d4e3ec014 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -2,6 +2,7 @@ import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/co
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { MetaService } from '@ngx-meta/core' 3import { MetaService } from '@ngx-meta/core'
4import { NotificationsService } from 'angular2-notifications' 4import { NotificationsService } from 'angular2-notifications'
5import { VideoService } from 'app/shared/video/video.service'
5import { Observable } from 'rxjs/Observable' 6import { Observable } from 'rxjs/Observable'
6import { Subscription } from 'rxjs/Subscription' 7import { Subscription } from 'rxjs/Subscription'
7import videojs from 'video.js' 8import videojs from 'video.js'
@@ -9,7 +10,10 @@ import { UserVideoRateType, VideoRateType } from '../../../../../shared'
9import '../../../assets/player/peertube-videojs-plugin' 10import '../../../assets/player/peertube-videojs-plugin'
10import { AuthService, ConfirmService } from '../../core' 11import { AuthService, ConfirmService } from '../../core'
11import { VideoBlacklistService } from '../../shared' 12import { VideoBlacklistService } from '../../shared'
12import { MarkdownService, VideoDetails, VideoService } from '../shared' 13import { Account } from '../../shared/account/account.model'
14import { VideoDetails } from '../../shared/video/video-details.model'
15import { Video } from '../../shared/video/video.model'
16import { MarkdownService } from '../shared'
13import { VideoDownloadComponent } from './video-download.component' 17import { VideoDownloadComponent } from './video-download.component'
14import { VideoReportComponent } from './video-report.component' 18import { VideoReportComponent } from './video-report.component'
15import { VideoShareComponent } from './video-share.component' 19import { VideoShareComponent } from './video-share.component'
@@ -24,13 +28,12 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
24 @ViewChild('videoShareModal') videoShareModal: VideoShareComponent 28 @ViewChild('videoShareModal') videoShareModal: VideoShareComponent
25 @ViewChild('videoReportModal') videoReportModal: VideoReportComponent 29 @ViewChild('videoReportModal') videoReportModal: VideoReportComponent
26 30
27 downloadSpeed: number 31 otherVideos: Video[] = []
32
28 error = false 33 error = false
29 loading = false 34 loading = false
30 numPeers: number
31 player: videojs.Player 35 player: videojs.Player
32 playerElement: HTMLMediaElement 36 playerElement: HTMLMediaElement
33 uploadSpeed: number
34 userRating: UserVideoRateType = null 37 userRating: UserVideoRateType = null
35 video: VideoDetails = null 38 video: VideoDetails = null
36 videoPlayerLoaded = false 39 videoPlayerLoaded = false
@@ -58,6 +61,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
58 ) {} 61 ) {}
59 62
60 ngOnInit () { 63 ngOnInit () {
64 this.videoService.getVideos({ currentPage: 1, itemsPerPage: 5 }, '-createdAt')
65 .subscribe(
66 data => this.otherVideos = data.videos,
67
68 err => console.error(err)
69 )
70
61 this.paramsSub = this.route.params.subscribe(routeParams => { 71 this.paramsSub = this.route.params.subscribe(routeParams => {
62 let uuid = routeParams['uuid'] 72 let uuid = routeParams['uuid']
63 this.videoService.getVideo(uuid).subscribe( 73 this.videoService.getVideo(uuid).subscribe(
@@ -115,27 +125,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
115 ) 125 )
116 } 126 }
117 127
118 removeVideo (event: Event) {
119 event.preventDefault()
120
121 this.confirmService.confirm('Do you really want to delete this video?', 'Delete').subscribe(
122 res => {
123 if (res === false) return
124
125 this.videoService.removeVideo(this.video.id)
126 .subscribe(
127 status => {
128 this.notificationsService.success('Success', `Video ${this.video.name} deleted.`)
129 // Go back to the video-list.
130 this.router.navigate(['/videos/list'])
131 },
132
133 error => this.notificationsService.error('Error', error.text)
134 )
135 }
136 )
137 }
138
139 blacklistVideo (event: Event) { 128 blacklistVideo (event: Event) {
140 event.preventDefault() 129 event.preventDefault()
141 130
@@ -166,7 +155,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
166 } 155 }
167 156
168 showLessDescription () { 157 showLessDescription () {
169
170 this.updateVideoDescription(this.shortVideoDescription) 158 this.updateVideoDescription(this.shortVideoDescription)
171 this.completeDescriptionShown = false 159 this.completeDescriptionShown = false
172 } 160 }
@@ -211,16 +199,18 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
211 return this.authService.isLoggedIn() 199 return this.authService.isLoggedIn()
212 } 200 }
213 201
214 canUserUpdateVideo () { 202 isVideoBlacklistable () {
215 return this.video.isUpdatableBy(this.authService.getUser()) 203 return this.video.isBlackistableBy(this.authService.getUser())
216 } 204 }
217 205
218 isVideoRemovable () { 206 getAvatarPath () {
219 return this.video.isRemovableBy(this.authService.getUser()) 207 return Account.GET_ACCOUNT_AVATAR_PATH(this.video.account)
220 } 208 }
221 209
222 isVideoBlacklistable () { 210 getVideoTags () {
223 return this.video.isBlackistableBy(this.authService.getUser()) 211 if (!this.video || Array.isArray(this.video.tags) === false) return []
212
213 return this.video.tags.join(', ')
224 } 214 }
225 215
226 private updateVideoDescription (description: string) { 216 private updateVideoDescription (description: string) {
@@ -229,6 +219,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
229 } 219 }
230 220
231 private setVideoDescriptionHTML () { 221 private setVideoDescriptionHTML () {
222 if (!this.video.description) {
223 this.videoHTMLDescription = ''
224 return
225 }
226
232 this.videoHTMLDescription = this.markdownService.markdownToHTML(this.video.description) 227 this.videoHTMLDescription = this.markdownService.markdownToHTML(this.video.description)
233 } 228 }
234 229
@@ -281,7 +276,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
281 return this.router.navigate([ '/videos/list' ]) 276 return this.router.navigate([ '/videos/list' ])
282 } 277 }
283 278
284 this.playerElement = this.elementRef.nativeElement.querySelector('#video-container') 279 this.playerElement = this.elementRef.nativeElement.querySelector('#video-element')
285 280
286 const videojsOptions = { 281 const videojsOptions = {
287 controls: true, 282 controls: true,
@@ -304,12 +299,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
304 this.on('customError', (event, data) => { 299 this.on('customError', (event, data) => {
305 self.handleError(data.err) 300 self.handleError(data.err)
306 }) 301 })
307
308 this.on('torrentInfo', (event, data) => {
309 self.downloadSpeed = data.downloadSpeed
310 self.numPeers = data.numPeers
311 self.uploadSpeed = data.uploadSpeed
312 })
313 }) 302 })
314 303
315 this.setVideoDescriptionHTML() 304 this.setVideoDescriptionHTML()
diff --git a/client/src/app/videos/+video-watch/video-watch.module.ts b/client/src/app/videos/+video-watch/video-watch.module.ts
index 1b983200d..0b1dd5c15 100644
--- a/client/src/app/videos/+video-watch/video-watch.module.ts
+++ b/client/src/app/videos/+video-watch/video-watch.module.ts
@@ -1,7 +1,7 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2 2
3import { VideoWatchRoutingModule } from './video-watch-routing.module' 3import { VideoWatchRoutingModule } from './video-watch-routing.module'
4import { VideoService, MarkdownService } from '../shared' 4import { MarkdownService } from '../shared'
5import { SharedModule } from '../../shared' 5import { SharedModule } from '../../shared'
6 6
7import { VideoWatchComponent } from './video-watch.component' 7import { VideoWatchComponent } from './video-watch.component'
@@ -28,8 +28,7 @@ import { VideoDownloadComponent } from './video-download.component'
28 ], 28 ],
29 29
30 providers: [ 30 providers: [
31 MarkdownService, 31 MarkdownService
32 VideoService
33 ] 32 ]
34}) 33})
35export class VideoWatchModule { } 34export class VideoWatchModule { }
diff --git a/client/src/app/videos/shared/index.ts b/client/src/app/videos/shared/index.ts
index 3f1458088..7a66944b9 100644
--- a/client/src/app/videos/shared/index.ts
+++ b/client/src/app/videos/shared/index.ts
@@ -1,8 +1 @@
1export * from './sort-field.type'
2export * from './markdown.service' export * from './markdown.service'
3export * from './video.model'
4export * from './video-details.model'
5export * from './video-edit.model'
6export * from './video.service'
7export * from './video-description.component'
8export * from './video-pagination.model'
diff --git a/client/src/app/videos/shared/video-description.component.scss b/client/src/app/videos/shared/video-description.component.scss
deleted file mode 100644
index d8d73e846..000000000
--- a/client/src/app/videos/shared/video-description.component.scss
+++ /dev/null
@@ -1,15 +0,0 @@
1textarea {
2 height: 150px;
3}
4
5.previews /deep/ {
6 .nav {
7 margin-top: 10px;
8 font-size: 0.9em;
9 }
10
11 .tab-content {
12 min-height: 75px;
13 padding: 5px;
14 }
15}
diff --git a/client/src/app/videos/video-list/index.ts b/client/src/app/videos/video-list/index.ts
index ed2bb1657..5e7c7886c 100644
--- a/client/src/app/videos/video-list/index.ts
+++ b/client/src/app/videos/video-list/index.ts
@@ -1,3 +1,3 @@
1export * from './my-videos.component' 1export * from './video-recently-added.component'
2export * from './video-list.component' 2export * from './video-trending.component'
3export * from './shared' 3export * from './video-search.component'
diff --git a/client/src/app/videos/video-list/my-videos.component.ts b/client/src/app/videos/video-list/my-videos.component.ts
deleted file mode 100644
index 648741a40..000000000
--- a/client/src/app/videos/video-list/my-videos.component.ts
+++ /dev/null
@@ -1,36 +0,0 @@
1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3
4import { NotificationsService } from 'angular2-notifications'
5
6import { AbstractVideoList } from './shared'
7import { VideoService } from '../shared'
8
9@Component({
10 selector: 'my-videos',
11 styleUrls: [ './shared/abstract-video-list.scss' ],
12 templateUrl: './shared/abstract-video-list.html'
13})
14export class MyVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
15
16 constructor (
17 protected router: Router,
18 protected route: ActivatedRoute,
19 protected notificationsService: NotificationsService,
20 private videoService: VideoService
21 ) {
22 super()
23 }
24
25 ngOnInit () {
26 super.ngOnInit()
27 }
28
29 ngOnDestroy () {
30 this.subActivatedRoute.unsubscribe()
31 }
32
33 getVideosObservable () {
34 return this.videoService.getMyVideos(this.pagination, this.sort)
35 }
36}
diff --git a/client/src/app/videos/video-list/shared/abstract-video-list.html b/client/src/app/videos/video-list/shared/abstract-video-list.html
deleted file mode 100644
index 680fba3f5..000000000
--- a/client/src/app/videos/video-list/shared/abstract-video-list.html
+++ /dev/null
@@ -1,28 +0,0 @@
1<div class="row">
2 <div class="content-padding">
3 <div class="videos-info">
4 <div class="col-md-9 col-xs-5 videos-total-results">
5 <span *ngIf="pagination.totalItems !== null">{{ pagination.totalItems }} videos</span>
6
7 <my-loader [loading]="loading | async"></my-loader>
8 </div>
9
10 <my-video-sort class="col-md-3 col-xs-7" [currentSort]="sort" (sort)="onSort($event)"></my-video-sort>
11 </div>
12 </div>
13</div>
14
15<div class="content-padding videos-miniatures">
16 <div class="no-video" *ngIf="isThereNoVideo()">There is no video.</div>
17
18 <my-video-miniature
19 class="ng-animate"
20 *ngFor="let video of videos" [video]="video" [user]="user" [currentSort]="sort"
21 >
22 </my-video-miniature>
23</div>
24
25<pagination *ngIf="pagination.totalItems !== null && pagination.totalItems !== 0"
26 [totalItems]="pagination.totalItems" [itemsPerPage]="pagination.itemsPerPage" [maxSize]="6" [boundaryLinks]="true" [rotate]="false"
27 [(ngModel)]="pagination.currentPage" (pageChanged)="onPageChanged($event)"
28></pagination>
diff --git a/client/src/app/videos/video-list/shared/abstract-video-list.scss b/client/src/app/videos/video-list/shared/abstract-video-list.scss
deleted file mode 100644
index 4b4409602..000000000
--- a/client/src/app/videos/video-list/shared/abstract-video-list.scss
+++ /dev/null
@@ -1,37 +0,0 @@
1.videos-info {
2 @media screen and (max-width: 400px) {
3 margin-left: 0;
4 }
5
6 border-bottom: 1px solid #f1f1f1;
7 height: 40px;
8 line-height: 40px;
9
10 .videos-total-results {
11 font-size: 13px;
12 }
13
14 my-loader {
15 display: inline-block;
16 margin-left: 5px;
17 }
18}
19
20.videos-miniatures {
21 text-align: center;
22 padding-top: 0;
23
24 my-video-miniature {
25 text-align: left;
26 }
27
28 .no-video {
29 margin-top: 50px;
30 text-align: center;
31 }
32}
33
34pagination {
35 display: block;
36 text-align: center;
37}
diff --git a/client/src/app/videos/video-list/shared/abstract-video-list.ts b/client/src/app/videos/video-list/shared/abstract-video-list.ts
deleted file mode 100644
index 87d5bc48a..000000000
--- a/client/src/app/videos/video-list/shared/abstract-video-list.ts
+++ /dev/null
@@ -1,104 +0,0 @@
1import { OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { Subscription } from 'rxjs/Subscription'
4import { BehaviorSubject } from 'rxjs/BehaviorSubject'
5import { Observable } from 'rxjs/Observable'
6
7import { NotificationsService } from 'angular2-notifications'
8
9import {
10 SortField,
11 Video,
12 VideoPagination
13} from '../../shared'
14
15export abstract class AbstractVideoList implements OnInit, OnDestroy {
16 loading: BehaviorSubject<boolean> = new BehaviorSubject(false)
17 pagination: VideoPagination = {
18 currentPage: 1,
19 itemsPerPage: 25,
20 totalItems: null
21 }
22 sort: SortField
23 videos: Video[] = []
24
25 protected notificationsService: NotificationsService
26 protected router: Router
27 protected route: ActivatedRoute
28
29 protected subActivatedRoute: Subscription
30
31 abstract getVideosObservable (): Observable<{ videos: Video[], totalVideos: number}>
32
33 ngOnInit () {
34 // Subscribe to route changes
35 this.subActivatedRoute = this.route.params.subscribe(routeParams => {
36 this.loadRouteParams(routeParams)
37
38 this.getVideos()
39 })
40 }
41
42 ngOnDestroy () {
43 this.subActivatedRoute.unsubscribe()
44 }
45
46 getVideos () {
47 this.loading.next(true)
48 this.videos = []
49
50 const observable = this.getVideosObservable()
51
52 observable.subscribe(
53 ({ videos, totalVideos }) => {
54 this.videos = videos
55 this.pagination.totalItems = totalVideos
56
57 this.loading.next(false)
58 },
59 error => this.notificationsService.error('Error', error.text)
60 )
61 }
62
63 isThereNoVideo () {
64 return !this.loading.getValue() && this.videos.length === 0
65 }
66
67 onPageChanged (event: { page: number }) {
68 // Be sure the current page is set
69 this.pagination.currentPage = event.page
70
71 this.navigateToNewParams()
72 }
73
74 onSort (sort: SortField) {
75 this.sort = sort
76
77 this.navigateToNewParams()
78 }
79
80 protected buildRouteParams () {
81 // There is always a sort and a current page
82 const params = {
83 sort: this.sort,
84 page: this.pagination.currentPage
85 }
86
87 return params
88 }
89
90 protected loadRouteParams (routeParams: { [ key: string ]: any }) {
91 this.sort = routeParams['sort'] as SortField || '-createdAt'
92
93 if (routeParams['page'] !== undefined) {
94 this.pagination.currentPage = parseInt(routeParams['page'], 10)
95 } else {
96 this.pagination.currentPage = 1
97 }
98 }
99
100 protected navigateToNewParams () {
101 const routeParams = this.buildRouteParams()
102 this.router.navigate([ '/videos/list', routeParams ])
103 }
104}
diff --git a/client/src/app/videos/video-list/shared/index.ts b/client/src/app/videos/video-list/shared/index.ts
deleted file mode 100644
index d8f73bcda..000000000
--- a/client/src/app/videos/video-list/shared/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
1export * from './abstract-video-list'
2export * from './video-miniature.component'
3export * from './video-sort.component'
diff --git a/client/src/app/videos/video-list/shared/video-miniature.component.html b/client/src/app/videos/video-list/shared/video-miniature.component.html
deleted file mode 100644
index 6bbd29666..000000000
--- a/client/src/app/videos/video-list/shared/video-miniature.component.html
+++ /dev/null
@@ -1,33 +0,0 @@
1<div class="video-miniature">
2 <a
3 [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.description"
4 class="video-miniature-thumbnail"
5 >
6 <img [attr.src]="video.thumbnailUrl" alt="video thumbnail" [ngClass]="{ 'blur-filter': isVideoNSFWForThisUser() }" />
7
8 <div class="video-miniature-thumbnail-overlay">
9 <span class="video-miniature-thumbnail-overlay-views">{{ video.views }} views</span>
10 <span class="video-miniature-thumbnail-overlay-duration">{{ video.durationLabel }}</span>
11 </div>
12 </a>
13
14 <div class="video-miniature-information">
15 <span class="video-miniature-name">
16 <a
17 class="video-miniature-name"
18 [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoNSFWForThisUser() }"
19 >
20 {{ video.name }}
21 </a>
22 </span>
23
24 <div class="video-miniature-tags">
25 <span *ngFor="let tag of video.tags" class="video-miniature-tag">
26 <a [routerLink]="['/videos/list', { field: 'tags', search: tag, sort: currentSort }]" class="label label-primary">{{ tag }}</a>
27 </span>
28 </div>
29
30 <a [routerLink]="['/videos/list', { field: 'account', search: video.account, sort: currentSort }]" class="video-miniature-account">{{ video.by }}</a>
31 <span class="video-miniature-created-at">{{ video.createdAt | date:'short' }}</span>
32 </div>
33</div>
diff --git a/client/src/app/videos/video-list/shared/video-miniature.component.scss b/client/src/app/videos/video-list/shared/video-miniature.component.scss
deleted file mode 100644
index 507ace098..000000000
--- a/client/src/app/videos/video-list/shared/video-miniature.component.scss
+++ /dev/null
@@ -1,101 +0,0 @@
1.video-miniature {
2 margin: 15px 10px;
3 display: inline-block;
4 position: relative;
5 height: 190px;
6 vertical-align: top;
7
8 .video-miniature-thumbnail {
9 display: inline-block;
10 position: relative;
11 border-radius: 3px;
12 overflow: hidden;
13
14 &:hover {
15 text-decoration: none !important;
16 }
17
18 img.blur-filter {
19 filter: blur(5px);
20 transform : scale(1.03);
21 }
22
23 .video-miniature-thumbnail-overlay {
24 position: absolute;
25 right: 0px;
26 bottom: 0px;
27 display: inline-block;
28 background-color: rgba(0, 0, 0, 0.7);
29 color: #fff;
30 padding: 3px 5px;
31 font-size: 11px;
32 font-weight: bold;
33 width: 100%;
34
35 .video-miniature-thumbnail-overlay-views {
36
37 }
38
39 .video-miniature-thumbnail-overlay-duration {
40 float: right;
41 }
42 }
43 }
44
45 .video-miniature-information {
46 width: 200px;
47
48 .video-miniature-name {
49 height: 23px;
50 display: block;
51 overflow: hidden;
52 text-overflow: ellipsis;
53 white-space: nowrap;
54 font-weight: bold;
55 transition: color 0.2s;
56 font-size: 15px;
57
58 &:hover {
59 text-decoration: none;
60 }
61
62 &.blur-filter {
63 filter: blur(3px);
64 padding-left: 4px;
65 }
66
67 .video-miniature-tags {
68 // Fix for chrome when tags are long
69 width: 201px;
70
71 .video-miniature-tag {
72 font-size: 13px;
73 cursor: pointer;
74 position: relative;
75 top: -2px;
76
77 .label {
78 transition: background-color 0.2s;
79 }
80 }
81 }
82 }
83
84 .video-miniature-account, .video-miniature-created-at {
85 display: block;
86 margin-left: 1px;
87 font-size: 11px;
88 color: $video-miniature-other-infos;
89 opacity: 0.9;
90 }
91
92 .video-miniature-account {
93 transition: color 0.2s;
94
95 &:hover {
96 color: #23527c;
97 text-decoration: none;
98 }
99 }
100 }
101}
diff --git a/client/src/app/videos/video-list/shared/video-sort.component.html b/client/src/app/videos/video-list/shared/video-sort.component.html
deleted file mode 100644
index 3bece0b22..000000000
--- a/client/src/app/videos/video-list/shared/video-sort.component.html
+++ /dev/null
@@ -1,5 +0,0 @@
1<select class="form-control input-sm" [(ngModel)]="currentSort" (ngModelChange)="onSortChange()">
2 <option *ngFor="let choice of choiceKeys" [value]="choice">
3 {{ getStringChoice(choice) }}
4 </option>
5</select>
diff --git a/client/src/app/videos/video-list/shared/video-sort.component.ts b/client/src/app/videos/video-list/shared/video-sort.component.ts
deleted file mode 100644
index 8aa89d32b..000000000
--- a/client/src/app/videos/video-list/shared/video-sort.component.ts
+++ /dev/null
@@ -1,39 +0,0 @@
1import { Component, EventEmitter, Input, Output } from '@angular/core'
2
3import { SortField } from '../../shared'
4
5@Component({
6 selector: 'my-video-sort',
7 templateUrl: './video-sort.component.html'
8})
9
10export class VideoSortComponent {
11 @Output() sort = new EventEmitter<any>()
12
13 @Input() currentSort: SortField
14
15 sortChoices: { [ P in SortField ]: string } = {
16 'name': 'Name - Asc',
17 '-name': 'Name - Desc',
18 'duration': 'Duration - Asc',
19 '-duration': 'Duration - Desc',
20 'createdAt': 'Created Date - Asc',
21 '-createdAt': 'Created Date - Desc',
22 'views': 'Views - Asc',
23 '-views': 'Views - Desc',
24 'likes': 'Likes - Asc',
25 '-likes': 'Likes - Desc'
26 }
27
28 get choiceKeys () {
29 return Object.keys(this.sortChoices)
30 }
31
32 getStringChoice (choiceKey: SortField) {
33 return this.sortChoices[choiceKey]
34 }
35
36 onSortChange () {
37 this.sort.emit(this.currentSort)
38 }
39}
diff --git a/client/src/app/videos/video-list/video-list.component.ts b/client/src/app/videos/video-list/video-list.component.ts
deleted file mode 100644
index 784162679..000000000
--- a/client/src/app/videos/video-list/video-list.component.ts
+++ /dev/null
@@ -1,94 +0,0 @@
1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { Subscription } from 'rxjs/Subscription'
4
5import { NotificationsService } from 'angular2-notifications'
6
7import { VideoService } from '../shared'
8import { Search, SearchField, SearchService } from '../../shared'
9import { AbstractVideoList } from './shared'
10
11@Component({
12 selector: 'my-videos-list',
13 styleUrls: [ './shared/abstract-video-list.scss' ],
14 templateUrl: './shared/abstract-video-list.html'
15})
16export class VideoListComponent extends AbstractVideoList implements OnInit, OnDestroy {
17 private search: Search
18 private subSearch: Subscription
19
20 constructor (
21 protected router: Router,
22 protected route: ActivatedRoute,
23 protected notificationsService: NotificationsService,
24 private videoService: VideoService,
25 private searchService: SearchService
26 ) {
27 super()
28 }
29
30 ngOnInit () {
31 // Subscribe to route changes
32 this.subActivatedRoute = this.route.params.subscribe(routeParams => {
33 this.loadRouteParams(routeParams)
34
35 // Update the search service component
36 this.searchService.updateSearch.next(this.search)
37 this.getVideos()
38 })
39
40 // Subscribe to search changes
41 this.subSearch = this.searchService.searchUpdated.subscribe(search => {
42 this.search = search
43 // Reset pagination
44 this.pagination.currentPage = 1
45
46 this.navigateToNewParams()
47 })
48 }
49
50 ngOnDestroy () {
51 super.ngOnDestroy()
52
53 this.subSearch.unsubscribe()
54 }
55
56 getVideosObservable () {
57 let observable = null
58 if (this.search.value) {
59 observable = this.videoService.searchVideos(this.search, this.pagination, this.sort)
60 } else {
61 observable = this.videoService.getVideos(this.pagination, this.sort)
62 }
63
64 return observable
65 }
66
67 protected buildRouteParams () {
68 const params = super.buildRouteParams()
69
70 // Maybe there is a search
71 if (this.search.value) {
72 params['field'] = this.search.field
73 params['search'] = this.search.value
74 }
75
76 return params
77 }
78
79 protected loadRouteParams (routeParams: { [ key: string ]: any }) {
80 super.loadRouteParams(routeParams)
81
82 if (routeParams['search'] !== undefined) {
83 this.search = {
84 value: routeParams['search'],
85 field: routeParams['field'] as SearchField
86 }
87 } else {
88 this.search = {
89 value: '',
90 field: 'name'
91 }
92 }
93 }
94}
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
new file mode 100644
index 000000000..6168fac95
--- /dev/null
+++ b/client/src/app/videos/video-list/video-recently-added.component.ts
@@ -0,0 +1,32 @@
1import { Component, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { NotificationsService } from 'angular2-notifications'
4import { AbstractVideoList } from '../../shared/video/abstract-video-list'
5import { SortField } from '../../shared/video/sort-field.type'
6import { VideoService } from '../../shared/video/video.service'
7
8@Component({
9 selector: 'my-videos-recently-added',
10 styleUrls: [ '../../shared/video/abstract-video-list.scss' ],
11 templateUrl: '../../shared/video/abstract-video-list.html'
12})
13export class VideoRecentlyAddedComponent extends AbstractVideoList implements OnInit {
14 titlePage = 'Recently added'
15 currentRoute = '/videos/recently-added'
16 sort: SortField = '-createdAt'
17
18 constructor (protected router: Router,
19 protected route: ActivatedRoute,
20 protected notificationsService: NotificationsService,
21 private videoService: VideoService) {
22 super()
23 }
24
25 ngOnInit () {
26 super.ngOnInit()
27 }
28
29 getVideosObservable () {
30 return this.videoService.getVideos(this.pagination, this.sort)
31 }
32}
diff --git a/client/src/app/videos/video-list/video-search.component.ts b/client/src/app/videos/video-list/video-search.component.ts
new file mode 100644
index 000000000..ba851d27e
--- /dev/null
+++ b/client/src/app/videos/video-list/video-search.component.ts
@@ -0,0 +1,51 @@
1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { NotificationsService } from 'angular2-notifications'
4import { AbstractVideoList } from 'app/shared/video/abstract-video-list'
5import { Subscription } from 'rxjs/Subscription'
6import { SortField } from '../../shared/video/sort-field.type'
7import { VideoService } from '../../shared/video/video.service'
8
9@Component({
10 selector: 'my-videos-search',
11 styleUrls: [ '../../shared/video/abstract-video-list.scss' ],
12 templateUrl: '../../shared/video/abstract-video-list.html'
13})
14export class VideoSearchComponent extends AbstractVideoList implements OnInit, OnDestroy {
15 titlePage = 'Search'
16 currentRoute = '/videos/search'
17 loadOnInit = false
18
19 private search = ''
20 private subActivatedRoute: Subscription
21
22 constructor (protected router: Router,
23 protected route: ActivatedRoute,
24 protected notificationsService: NotificationsService,
25 private videoService: VideoService) {
26 super()
27 }
28
29 ngOnInit () {
30 super.ngOnInit()
31
32 this.subActivatedRoute = this.route.queryParams.subscribe(
33 queryParams => {
34 this.search = queryParams['search']
35 this.reloadVideos()
36 },
37
38 err => this.notificationsService.error('Error', err.text)
39 )
40 }
41
42 ngOnDestroy () {
43 if (this.subActivatedRoute) {
44 this.subActivatedRoute.unsubscribe()
45 }
46 }
47
48 getVideosObservable () {
49 return this.videoService.searchVideos(this.search, this.pagination, this.sort)
50 }
51}
diff --git a/client/src/app/videos/video-list/video-trending.component.ts b/client/src/app/videos/video-list/video-trending.component.ts
new file mode 100644
index 000000000..e80fd7f2c
--- /dev/null
+++ b/client/src/app/videos/video-list/video-trending.component.ts
@@ -0,0 +1,32 @@
1import { Component, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { NotificationsService } from 'angular2-notifications'
4import { AbstractVideoList } from 'app/shared/video/abstract-video-list'
5import { SortField } from '../../shared/video/sort-field.type'
6import { VideoService } from '../../shared/video/video.service'
7
8@Component({
9 selector: 'my-videos-trending',
10 styleUrls: [ '../../shared/video/abstract-video-list.scss' ],
11 templateUrl: '../../shared/video/abstract-video-list.html'
12})
13export class VideoTrendingComponent extends AbstractVideoList implements OnInit {
14 titlePage = 'Trending'
15 currentRoute = '/videos/trending'
16 defaultSort: SortField = '-views'
17
18 constructor (protected router: Router,
19 protected route: ActivatedRoute,
20 protected notificationsService: NotificationsService,
21 private videoService: VideoService) {
22 super()
23 }
24
25 ngOnInit () {
26 super.ngOnInit()
27 }
28
29 getVideosObservable () {
30 return this.videoService.getVideos(this.pagination, this.sort)
31 }
32}
diff --git a/client/src/app/videos/videos-routing.module.ts b/client/src/app/videos/videos-routing.module.ts
index 3ca3e5486..6910421b7 100644
--- a/client/src/app/videos/videos-routing.module.ts
+++ b/client/src/app/videos/videos-routing.module.ts
@@ -1,9 +1,9 @@
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 { VideoSearchComponent } from './video-list'
6import { VideoListComponent, MyVideosComponent } from './video-list' 5import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component'
6import { VideoTrendingComponent } from './video-list/video-trending.component'
7import { VideosComponent } from './videos.component' 7import { VideosComponent } from './videos.component'
8 8
9const videosRoutes: Routes = [ 9const videosRoutes: Routes = [
@@ -13,20 +13,34 @@ const videosRoutes: Routes = [
13 canActivateChild: [ MetaGuard ], 13 canActivateChild: [ MetaGuard ],
14 children: [ 14 children: [
15 { 15 {
16 path: 'mine', 16 path: 'list',
17 component: MyVideosComponent, 17 pathMatch: 'full',
18 redirectTo: 'recently-added'
19 },
20 {
21 path: 'trending',
22 component: VideoTrendingComponent,
18 data: { 23 data: {
19 meta: { 24 meta: {
20 title: 'My videos' 25 title: 'Trending videos'
21 } 26 }
22 } 27 }
23 }, 28 },
24 { 29 {
25 path: 'list', 30 path: 'recently-added',
26 component: VideoListComponent, 31 component: VideoRecentlyAddedComponent,
32 data: {
33 meta: {
34 title: 'Recently added videos'
35 }
36 }
37 },
38 {
39 path: 'search',
40 component: VideoSearchComponent,
27 data: { 41 data: {
28 meta: { 42 meta: {
29 title: 'Videos list' 43 title: 'Search videos'
30 } 44 }
31 } 45 }
32 }, 46 },
@@ -50,6 +64,7 @@ const videosRoutes: Routes = [
50 }, 64 },
51 { 65 {
52 path: ':uuid', 66 path: ':uuid',
67 pathMatch: 'full',
53 redirectTo: 'watch/:uuid' 68 redirectTo: 'watch/:uuid'
54 }, 69 },
55 { 70 {
diff --git a/client/src/app/videos/videos.module.ts b/client/src/app/videos/videos.module.ts
index 4f3054c3a..4b14d1da8 100644
--- a/client/src/app/videos/videos.module.ts
+++ b/client/src/app/videos/videos.module.ts
@@ -1,7 +1,8 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { SharedModule } from '../shared' 2import { SharedModule } from '../shared'
3import { VideoService } from './shared' 3import { VideoSearchComponent } from './video-list'
4import { MyVideosComponent, VideoListComponent, VideoMiniatureComponent, VideoSortComponent } from './video-list' 4import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component'
5import { VideoTrendingComponent } from './video-list/video-trending.component'
5import { VideosRoutingModule } from './videos-routing.module' 6import { VideosRoutingModule } from './videos-routing.module'
6import { VideosComponent } from './videos.component' 7import { VideosComponent } from './videos.component'
7 8
@@ -14,18 +15,15 @@ import { VideosComponent } from './videos.component'
14 declarations: [ 15 declarations: [
15 VideosComponent, 16 VideosComponent,
16 17
17 VideoListComponent, 18 VideoTrendingComponent,
18 MyVideosComponent, 19 VideoRecentlyAddedComponent,
19 VideoMiniatureComponent, 20 VideoSearchComponent
20 VideoSortComponent
21 ], 21 ],
22 22
23 exports: [ 23 exports: [
24 VideosComponent 24 VideosComponent
25 ], 25 ],
26 26
27 providers: [ 27 providers: []
28 VideoService
29 ]
30}) 28})
31export class VideosModule { } 29export class VideosModule { }
diff --git a/client/src/assets/favicon.png b/client/src/assets/favicon.png
deleted file mode 100644
index bb57ee6b0..000000000
--- a/client/src/assets/favicon.png
+++ /dev/null
Binary files differ
diff --git a/client/src/assets/images/admin/add.svg b/client/src/assets/images/admin/add.svg
new file mode 100644
index 000000000..42b269c43
--- /dev/null
+++ b/client/src/assets/images/admin/add.svg
@@ -0,0 +1,13 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <defs></defs>
4 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
5 <g id="Artboard-4" transform="translate(-92.000000, -115.000000)">
6 <g id="2" transform="translate(92.000000, 115.000000)">
7 <circle id="Oval-1" stroke="#ffffff" stroke-width="2" cx="12" cy="12" r="10"></circle>
8 <rect id="Rectangle-1" fill="#ffffff" x="11" y="7" width="2" height="10" rx="1"></rect>
9 <rect id="Rectangle-1" fill="#ffffff" x="7" y="11" width="10" height="2" rx="1"></rect>
10 </g>
11 </g>
12 </g>
13</svg>
diff --git a/client/src/assets/images/default-avatar.png b/client/src/assets/images/default-avatar.png
new file mode 100644
index 000000000..4b7fd2c0a
--- /dev/null
+++ b/client/src/assets/images/default-avatar.png
Binary files differ
diff --git a/client/src/assets/images/favicon.png b/client/src/assets/images/favicon.png
new file mode 100644
index 000000000..2e589cf6c
--- /dev/null
+++ b/client/src/assets/images/favicon.png
Binary files differ
diff --git a/client/src/assets/images/global/delete-grey.svg b/client/src/assets/images/global/delete-grey.svg
new file mode 100644
index 000000000..67e9e2ce7
--- /dev/null
+++ b/client/src/assets/images/global/delete-grey.svg
@@ -0,0 +1,14 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <defs></defs>
4 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
5 <g id="Artboard-4" transform="translate(-224.000000, -159.000000)">
6 <g id="25" transform="translate(224.000000, 159.000000)">
7 <path d="M5,7 L5,20.0081158 C5,21.1082031 5.89706013,22 7.00585866,22 L16.9941413,22 C18.1019465,22 19,21.1066027 19,20.0081158 L19,7" id="Path-296" stroke="#585858" stroke-width="2"></path>
8 <rect id="Rectangle-424" fill="#585858" x="2" y="4" width="20" height="2" rx="1"></rect>
9 <path d="M9,10.9970301 C9,10.4463856 9.44386482,10 10,10 C10.5522847,10 11,10.4530363 11,10.9970301 L11,17.0029699 C11,17.5536144 10.5561352,18 10,18 C9.44771525,18 9,17.5469637 9,17.0029699 L9,10.9970301 Z M13,10.9970301 C13,10.4463856 13.4438648,10 14,10 C14.5522847,10 15,10.4530363 15,10.9970301 L15,17.0029699 C15,17.5536144 14.5561352,18 14,18 C13.4477153,18 13,17.5469637 13,17.0029699 L13,10.9970301 Z" id="Combined-Shape" fill="#585858"></path>
10 <path d="M9,5 L9,2.99895656 C9,2.44724809 9.45097518,2 9.99077797,2 L14.009222,2 C14.5564136,2 15,2.44266033 15,2.99895656 L15,5" id="Path-33" stroke="#585858" stroke-width="2" stroke-linejoin="round"></path>
11 </g>
12 </g>
13 </g>
14</svg>
diff --git a/client/src/assets/images/global/delete-white.svg b/client/src/assets/images/global/delete-white.svg
new file mode 100644
index 000000000..9c52de557
--- /dev/null
+++ b/client/src/assets/images/global/delete-white.svg
@@ -0,0 +1,14 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <defs></defs>
4 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
5 <g id="Artboard-4" transform="translate(-224.000000, -159.000000)">
6 <g id="25" transform="translate(224.000000, 159.000000)">
7 <path d="M5,7 L5,20.0081158 C5,21.1082031 5.89706013,22 7.00585866,22 L16.9941413,22 C18.1019465,22 19,21.1066027 19,20.0081158 L19,7" id="Path-296" stroke="#ffffff" stroke-width="2"></path>
8 <rect id="Rectangle-424" fill="#ffffff" x="2" y="4" width="20" height="2" rx="1"></rect>
9 <path d="M9,10.9970301 C9,10.4463856 9.44386482,10 10,10 C10.5522847,10 11,10.4530363 11,10.9970301 L11,17.0029699 C11,17.5536144 10.5561352,18 10,18 C9.44771525,18 9,17.5469637 9,17.0029699 L9,10.9970301 Z M13,10.9970301 C13,10.4463856 13.4438648,10 14,10 C14.5522847,10 15,10.4530363 15,10.9970301 L15,17.0029699 C15,17.5536144 14.5561352,18 14,18 C13.4477153,18 13,17.5469637 13,17.0029699 L13,10.9970301 Z" id="Combined-Shape" fill="#ffffff"></path>
10 <path d="M9,5 L9,2.99895656 C9,2.44724809 9.45097518,2 9.99077797,2 L14.009222,2 C14.5564136,2 15,2.44266033 15,2.99895656 L15,5" id="Path-33" stroke="#ffffff" stroke-width="2" stroke-linejoin="round"></path>
11 </g>
12 </g>
13 </g>
14</svg>
diff --git a/client/src/assets/images/global/edit.svg b/client/src/assets/images/global/edit.svg
new file mode 100644
index 000000000..23ece68f1
--- /dev/null
+++ b/client/src/assets/images/global/edit.svg
@@ -0,0 +1,15 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
4 <title>edit</title>
5 <desc>Created with Sketch.</desc>
6 <defs></defs>
7 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
8 <g id="Artboard-4" transform="translate(-48.000000, -203.000000)" stroke="#585858" stroke-width="2">
9 <g id="41" transform="translate(48.000000, 203.000000)">
10 <path d="M3,21.0000003 L3,17 L15.8898356,4.11016442 C17.0598483,2.9401517 18.9638992,2.94723715 20.1306896,4.11402752 L19.9181432,3.90148112 C21.0902894,5.07362738 21.0882407,6.97202708 19.9174652,8.1377941 L7,21.0000003 L3,21.0000003 Z" id="Path-74" stroke-linecap="round" stroke-linejoin="round"></path>
11 <path d="M14.5,5.5 L18.5,9.5" id="Path-75"></path>
12 </g>
13 </g>
14 </g>
15</svg>
diff --git a/client/src/assets/images/global/validate.svg b/client/src/assets/images/global/validate.svg
new file mode 100644
index 000000000..5c7ee9d14
--- /dev/null
+++ b/client/src/assets/images/global/validate.svg
@@ -0,0 +1,14 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <defs></defs>
4 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
5 <g id="Artboard-4" transform="translate(-400.000000, -1134.000000)" stroke="#ffffff" stroke-width="2">
6 <g id="Extras" transform="translate(48.000000, 1046.000000)">
7 <g id="yes" transform="translate(352.000000, 88.000000)">
8 <circle id="Oval-1" cx="12" cy="12" r="10"></circle>
9 <polyline id="Path-288" stroke-linecap="round" stroke-linejoin="round" points="8.5 12.5 10.5 14.5 15.5 9.5"></polyline>
10 </g>
11 </g>
12 </g>
13 </g>
14</svg>
diff --git a/client/src/assets/images/header/menu.svg b/client/src/assets/images/header/menu.svg
new file mode 100644
index 000000000..7101bf73b
--- /dev/null
+++ b/client/src/assets/images/header/menu.svg
@@ -0,0 +1,14 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
4 <title>menu</title>
5 <desc>Created with Sketch.</desc>
6 <defs></defs>
7 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
8 <g id="Artboard-4" transform="translate(-180.000000, -203.000000)" stroke="#333333">
9 <g id="44" transform="translate(180.000000, 203.000000)">
10 <path d="M3.5,7 C3.5,6.72319836 3.72175357,6.5 3.99339768,6.5 L20.0066023,6.5 C20.2799786,6.5 20.5,6.72089465 20.5,7 C20.5,7.27680164 20.2782464,7.5 20.0066023,7.5 L3.99339768,7.5 C3.72002141,7.5 3.5,7.27910535 3.5,7 Z M3.5,12 C3.5,11.7231984 3.72175357,11.5 3.99339768,11.5 L20.0066023,11.5 C20.2799786,11.5 20.5,11.7208946 20.5,12 C20.5,12.2768016 20.2782464,12.5 20.0066023,12.5 L3.99339768,12.5 C3.72002141,12.5 3.5,12.2791054 3.5,12 Z M3.5,17 C3.5,16.7231984 3.72175357,16.5 3.99339768,16.5 L20.0066023,16.5 C20.2799786,16.5 20.5,16.7208946 20.5,17 C20.5,17.2768016 20.2782464,17.5 20.0066023,17.5 L3.99339768,17.5 C3.72002141,17.5 3.5,17.2791054 3.5,17 Z" id="Combined-Shape"></path>
11 </g>
12 </g>
13 </g>
14</svg> \ No newline at end of file
diff --git a/client/src/assets/images/header/search.svg b/client/src/assets/images/header/search.svg
new file mode 100644
index 000000000..489b59e9b
--- /dev/null
+++ b/client/src/assets/images/header/search.svg
@@ -0,0 +1,12 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <defs></defs>
4 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
5 <g id="Artboard-4" transform="translate(-136.000000, -115.000000)" stroke="#000" stroke-width="2">
6 <g id="3" transform="translate(136.000000, 115.000000)">
7 <circle id="Oval-3" cx="10" cy="10" r="7"></circle>
8 <path d="M15,15 L21,21" id="Path-3" stroke-linecap="round" stroke-linejoin="round"></path>
9 </g>
10 </g>
11 </g>
12</svg>
diff --git a/client/src/assets/images/header/upload.svg b/client/src/assets/images/header/upload.svg
new file mode 100644
index 000000000..2b07caf76
--- /dev/null
+++ b/client/src/assets/images/header/upload.svg
@@ -0,0 +1,16 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
4 <title>cloud-upload</title>
5 <desc>Created with Sketch.</desc>
6 <defs></defs>
7 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
8 <g id="Artboard-4" transform="translate(-312.000000, -775.000000)" stroke="#fff" stroke-width="2">
9 <g id="307" transform="translate(312.000000, 775.000000)">
10 <path d="M8,18 L5,18 L5,18 C2.790861,18 1,16.209139 1,14 C1,11.790861 2.790861,10 5,10 C5.35840468,10 5.70579988,10.0471371 6.03632437,10.1355501 C6.01233106,9.92702603 6,9.71495305 6,9.5 C6,6.46243388 8.46243388,4 11.5,4 C14.0673313,4 16.2238156,5.7590449 16.8299648,8.1376465 C17.2052921,8.04765874 17.5970804,8 18,8 C20.7614237,8 23,10.2385763 23,13 C23,15.7614237 20.7614237,18 18,18 L16,18" id="Combined-Shape" stroke-linejoin="round"></path>
11 <path d="M12,13 L12,21" id="Path-58"></path>
12 <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 12.500000) scale(1, -1) translate(-12.000000, -12.500000) " points="15 11 12 14 9 11"></polyline>
13 </g>
14 </g>
15 </g>
16</svg>
diff --git a/client/src/assets/images/logo.svg b/client/src/assets/images/logo.svg
new file mode 100644
index 000000000..8777acd5b
--- /dev/null
+++ b/client/src/assets/images/logo.svg
@@ -0,0 +1,118 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<svg
3 xmlns:dc="http://purl.org/dc/elements/1.1/"
4 xmlns:cc="http://creativecommons.org/ns#"
5 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
6 xmlns:svg="http://www.w3.org/2000/svg"
7 xmlns="http://www.w3.org/2000/svg"
8 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
9 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10 viewBox="2799 -911 16 22"
11 version="1.1"
12 id="svg13"
13 sodipodi:docname="logo.svg"
14 width="16"
15 height="22"
16 inkscape:version="0.92.2 5c3e80d, 2017-08-06">
17 <metadata
18 id="metadata17">
19 <rdf:RDF>
20 <cc:Work
21 rdf:about="">
22 <dc:format>image/svg+xml</dc:format>
23 <dc:type
24 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
25 <dc:title></dc:title>
26 </cc:Work>
27 </rdf:RDF>
28 </metadata>
29 <sodipodi:namedview
30 pagecolor="#ffffff"
31 bordercolor="#666666"
32 borderopacity="1"
33 objecttolerance="10"
34 gridtolerance="10"
35 guidetolerance="10"
36 inkscape:pageopacity="0"
37 inkscape:pageshadow="2"
38 inkscape:window-width="1916"
39 inkscape:window-height="1040"
40 id="namedview15"
41 showgrid="false"
42 inkscape:zoom="29.790476"
43 inkscape:cx="-1.1827326"
44 inkscape:cy="12.088"
45 inkscape:window-x="0"
46 inkscape:window-y="18"
47 inkscape:window-maximized="0"
48 inkscape:current-layer="svg13" />
49 <defs
50 id="defs4">
51 <style
52 id="style2">
53 .cls-3 {
54 fill: #211f20;
55 }
56
57 .cls-4 {
58 fill: #737373;
59 }
60
61 .cls-5 {
62 fill: #f1680d;
63 }
64
65 .cls-6 {
66 fill: #fff;
67 }
68 </style>
69 </defs>
70 <g
71 id="Artboard_1"
72 data-name="Artboard – 1"
73 class="cls-1"
74 transform="translate(0.03356777,-1.9929667)">
75 <g
76 id="Symbol_3_1"
77 data-name="Symbol 3 – 1"
78 transform="translate(2759,-975)">
79 <g
80 id="Group_44"
81 data-name="Group 44"
82 transform="translate(0,2.333)">
83 <path
84 id="Path_4"
85 data-name="Path 4"
86 class="cls-3"
87 d="m -949,-500 v 10.667 l 8,-5.333"
88 transform="translate(989,564)"
89 inkscape:connector-curvature="0"
90 style="fill:#211f20" />
91 <path
92 id="Path_5"
93 data-name="Path 5"
94 class="cls-4"
95 d="m -949,-500 v 10.667 l 8,-5.333"
96 transform="translate(989,574.667)"
97 inkscape:connector-curvature="0"
98 style="fill:#737373" />
99 <path
100 id="Path_6"
101 data-name="Path 6"
102 class="cls-5"
103 d="m -949,-500 v 10.667 l 8,-5.333"
104 transform="translate(997,569.333)"
105 inkscape:connector-curvature="0"
106 style="fill:#f1680d" />
107 <path
108 id="Path_7"
109 data-name="Path 7"
110 class="cls-6"
111 d="M 0,0 V 10.667 L 8,5.333 Z"
112 transform="rotate(180,24,40)"
113 inkscape:connector-curvature="0"
114 style="fill:#ffffff" />
115 </g>
116 </g>
117 </g>
118</svg>
diff --git a/client/src/assets/images/menu/administration.svg b/client/src/assets/images/menu/administration.svg
new file mode 100644
index 000000000..b6da837d2
--- /dev/null
+++ b/client/src/assets/images/menu/administration.svg
@@ -0,0 +1,14 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
4 <title>filter</title>
5 <desc>Created with Sketch.</desc>
6 <defs></defs>
7 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
8 <g id="Artboard-4" transform="translate(-444.000000, -247.000000)" fill="#808080">
9 <g id="70" transform="translate(444.000000, 247.000000)">
10 <path d="M8.82929429,17 L20.0066023,17 C20.5552407,17 21,17.4438648 21,18 C21,18.5522847 20.5550537,19 20.0066023,19 L8.82929429,19 C8.41745788,20.1651924 7.30621883,21 6,21 C4.34314575,21 3,19.6568542 3,18 C3,16.3431458 4.34314575,15 6,15 C7.30621883,15 8.41745788,15.8348076 8.82929429,17 Z M9.17070571,13 L3.99339768,13 C3.44475929,13 3,12.5561352 3,12 C3,11.4477153 3.44494629,11 3.99339768,11 L9.17070571,11 C9.58254212,9.83480763 10.6937812,9 12,9 C13.3062188,9 14.4174579,9.83480763 14.8292943,11 L20.0066023,11 C20.5552407,11 21,11.4438648 21,12 C21,12.5522847 20.5550537,13 20.0066023,13 L14.8292943,13 C14.4174579,14.1651924 13.3062188,15 12,15 C10.6937812,15 9.58254212,14.1651924 9.17070571,13 Z M15.1659641,6.98648118 C15.1124525,6.99537358 15.05751,7 15.0014977,7 L3.99850233,7 C3.44704472,7 3,6.55613518 3,6 C3,5.44771525 3.44748943,5 3.99850233,5 L15.0014977,5 C15.0575314,5 15.1124871,5.00458274 15.1660053,5.01340035 C15.5740343,3.84121344 16.6887792,3 18,3 C19.6568542,3 21,4.34314575 21,6 C21,7.65685425 19.6568542,9 18,9 C16.688735,9 15.5739592,8.15872988 15.1659641,6.98648118 Z M18,7 C18.5522847,7 19,6.55228475 19,6 C19,5.44771525 18.5522847,5 18,5 C17.4477153,5 17,5.44771525 17,6 C17,6.55228475 17.4477153,7 18,7 Z M12,13 C12.5522847,13 13,12.5522847 13,12 C13,11.4477153 12.5522847,11 12,11 C11.4477153,11 11,11.4477153 11,12 C11,12.5522847 11.4477153,13 12,13 Z M6,19 C6.55228475,19 7,18.5522847 7,18 C7,17.4477153 6.55228475,17 6,17 C5.44771525,17 5,17.4477153 5,18 C5,18.5522847 5.44771525,19 6,19 Z" id="Combined-Shape"></path>
11 </g>
12 </g>
13 </g>
14</svg>
diff --git a/client/src/assets/images/menu/recently-added.svg b/client/src/assets/images/menu/recently-added.svg
new file mode 100644
index 000000000..6473837f8
--- /dev/null
+++ b/client/src/assets/images/menu/recently-added.svg
@@ -0,0 +1,13 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <defs></defs>
4 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
5 <g id="Artboard-4" transform="translate(-92.000000, -115.000000)">
6 <g id="2" transform="translate(92.000000, 115.000000)">
7 <circle id="Oval-1" stroke="#808080" stroke-width="2" cx="12" cy="12" r="10"></circle>
8 <rect id="Rectangle-1" fill="#808080" x="11" y="7" width="2" height="10" rx="1"></rect>
9 <rect id="Rectangle-1" fill="#808080" x="7" y="11" width="10" height="2" rx="1"></rect>
10 </g>
11 </g>
12 </g>
13</svg>
diff --git a/client/src/assets/images/menu/trending.svg b/client/src/assets/images/menu/trending.svg
new file mode 100644
index 000000000..ffc65cc04
--- /dev/null
+++ b/client/src/assets/images/menu/trending.svg
@@ -0,0 +1,16 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
4 <title>graph</title>
5 <desc>Created with Sketch.</desc>
6 <defs></defs>
7 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
8 <g id="Artboard-4" transform="translate(-444.000000, -203.000000)" stroke-width="2" stroke="#808080">
9 <g id="50" transform="translate(444.000000, 203.000000)">
10 <polyline id="Path-96" points="3 3 3 21.006249 21.0246733 21.006249"></polyline>
11 <polyline id="Path-101" points="6 18 11 12 14 13 19 7"></polyline>
12 <polygon id="Path-102" points="20 9 20 6 17 6"></polygon>
13 </g>
14 </g>
15 </g>
16</svg>
diff --git a/client/src/assets/images/video/alert.svg b/client/src/assets/images/video/alert.svg
new file mode 100644
index 000000000..6d3af029f
--- /dev/null
+++ b/client/src/assets/images/video/alert.svg
@@ -0,0 +1,16 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
4 <title>alert</title>
5 <desc>Created with Sketch.</desc>
6 <defs></defs>
7 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
8 <g id="Artboard-4" transform="translate(-48.000000, -467.000000)">
9 <g id="161" transform="translate(48.000000, 467.000000)">
10 <path d="M12.8715755,3.50973876 L12,1.96027114 L11.1284245,3.50973876 L2.12842446,19.5097388 L1.29015252,21 L3,21 L21,21 L22.7098475,21 L21.8715755,19.5097388 L12.8715755,3.50973876 Z" id="Triangle-2" stroke="#585858" stroke-width="2" stroke-linejoin="round"></path>
11 <path d="M12,17.75 C12.6903559,17.75 13.25,17.1903559 13.25,16.5 C13.25,15.8096441 12.6903559,15.25 12,15.25 C11.3096441,15.25 10.75,15.8096441 10.75,16.5 C10.75,17.1903559 11.3096441,17.75 12,17.75 Z" id="Oval-8" fill="#585858"></path>
12 <rect id="Rectangle-3" fill="#585858" x="11" y="9" width="2" height="5" rx="1"></rect>
13 </g>
14 </g>
15 </g>
16</svg>
diff --git a/client/src/assets/images/video/dislike-grey.svg b/client/src/assets/images/video/dislike-grey.svg
new file mode 100644
index 000000000..56a7908fb
--- /dev/null
+++ b/client/src/assets/images/video/dislike-grey.svg
@@ -0,0 +1,14 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <defs></defs>
4 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
5 <g id="Artboard-4" transform="translate(-752.000000, -1090.000000)" stroke="#585858" stroke-width="2">
6 <g id="Extras" transform="translate(48.000000, 1046.000000)">
7 <g id="thumbs-down" transform="translate(704.000000, 44.000000)">
8 <path d="M6,16 C6,18.5 6.5,21 8,21 L16.9938335,21 C17.5495239,21 18.1819788,20.5956028 18.4072817,20.0949295 L20.8562951,14.6526776 C21.7640882,12.6353595 20.7154925,11 18.5092545,11 L15.5,11 C15.5,11 18.5,5 15,5 C12.5,5 11.5,11 8,11 C6.5,11 6,13.5 6,16 Z" id="Path-188" stroke-linejoin="round" transform="translate(13.591488, 13.000000) scale(1, -1) translate(-13.591488, -13.000000) "></path>
9 <path d="M4,4.5 C4,4.5 3,7 3,10 C3,13 4,15.5 4,15.5" id="Path-189" transform="translate(3.500000, 10.000000) scale(1, -1) translate(-3.500000, -10.000000) "></path>
10 </g>
11 </g>
12 </g>
13 </g>
14</svg>
diff --git a/client/src/assets/images/video/dislike-white.svg b/client/src/assets/images/video/dislike-white.svg
new file mode 100644
index 000000000..cfc6eaa1f
--- /dev/null
+++ b/client/src/assets/images/video/dislike-white.svg
@@ -0,0 +1,14 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <defs></defs>
4 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
5 <g id="Artboard-4" transform="translate(-752.000000, -1090.000000)" stroke="#ffffff" stroke-width="2">
6 <g id="Extras" transform="translate(48.000000, 1046.000000)">
7 <g id="thumbs-down" transform="translate(704.000000, 44.000000)">
8 <path d="M6,16 C6,18.5 6.5,21 8,21 L16.9938335,21 C17.5495239,21 18.1819788,20.5956028 18.4072817,20.0949295 L20.8562951,14.6526776 C21.7640882,12.6353595 20.7154925,11 18.5092545,11 L15.5,11 C15.5,11 18.5,5 15,5 C12.5,5 11.5,11 8,11 C6.5,11 6,13.5 6,16 Z" id="Path-188" stroke-linejoin="round" transform="translate(13.591488, 13.000000) scale(1, -1) translate(-13.591488, -13.000000) "></path>
9 <path d="M4,4.5 C4,4.5 3,7 3,10 C3,13 4,15.5 4,15.5" id="Path-189" transform="translate(3.500000, 10.000000) scale(1, -1) translate(-3.500000, -10.000000) "></path>
10 </g>
11 </g>
12 </g>
13 </g>
14</svg>
diff --git a/client/src/assets/images/video/download-grey.svg b/client/src/assets/images/video/download-grey.svg
new file mode 100644
index 000000000..5b0cca5ef
--- /dev/null
+++ b/client/src/assets/images/video/download-grey.svg
@@ -0,0 +1,16 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
4 <title>download</title>
5 <desc>Created with Sketch.</desc>
6 <defs></defs>
7 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
8 <g id="Artboard-4" transform="translate(-180.000000, -291.000000)" stroke="#585858" stroke-width="2">
9 <g id="84" transform="translate(180.000000, 291.000000)">
10 <path d="M12,3 L12,15" id="Path-58"></path>
11 <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 14.000000) rotate(-270.000000) translate(-12.000000, -14.000000) " points="9 8 15 14 9 20"></polyline>
12 <path d="M3,18 L3,20.0590859 C3,20.6127331 3.44494889,21.0615528 3.99340349,21.0615528 L20.0067018,21.0615528 C20.5553434,21.0615528 21.0001052,20.6098102 21.0001051,20.0590859 L21.0001049,18" id="Path-12" stroke-linejoin="round"></path>
13 </g>
14 </g>
15 </g>
16</svg>
diff --git a/client/src/assets/images/video/download-white.svg b/client/src/assets/images/video/download-white.svg
new file mode 100644
index 000000000..0e66e06e8
--- /dev/null
+++ b/client/src/assets/images/video/download-white.svg
@@ -0,0 +1,16 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
4 <title>download</title>
5 <desc>Created with Sketch.</desc>
6 <defs></defs>
7 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
8 <g id="Artboard-4" transform="translate(-180.000000, -291.000000)" stroke="#ffffff" stroke-width="2">
9 <g id="84" transform="translate(180.000000, 291.000000)">
10 <path d="M12,3 L12,15" id="Path-58"></path>
11 <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 14.000000) rotate(-270.000000) translate(-12.000000, -14.000000) " points="9 8 15 14 9 20"></polyline>
12 <path d="M3,18 L3,20.0590859 C3,20.6127331 3.44494889,21.0615528 3.99340349,21.0615528 L20.0067018,21.0615528 C20.5553434,21.0615528 21.0001052,20.6098102 21.0001051,20.0590859 L21.0001049,18" id="Path-12" stroke-linejoin="round"></path>
13 </g>
14 </g>
15 </g>
16</svg>
diff --git a/client/src/assets/images/video/eye-closed.svg b/client/src/assets/images/video/eye-closed.svg
new file mode 100644
index 000000000..c5b739659
--- /dev/null
+++ b/client/src/assets/images/video/eye-closed.svg
@@ -0,0 +1,18 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <defs></defs>
4 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
5 <g id="Artboard-4" transform="translate(-796.000000, -1046.000000)" stroke="#585858" stroke-width="2">
6 <g id="Extras" transform="translate(48.000000, 1046.000000)">
7 <g id="eye-closed" transform="translate(760.000000, 12.000000) scale(1, -1) translate(-760.000000, -12.000000) translate(748.000000, 0.000000)">
8 <path d="M2,14 C2,14 5,7 12,7 C19,7 22,14 22,14" id="Path-80" stroke-linejoin="round"></path>
9 <path d="M12,7 L12,5" id="Path-81"></path>
10 <path d="M18,8.5 L19,7" id="Path-81"></path>
11 <path d="M21,12 L22.5,11" id="Path-81"></path>
12 <path d="M1.5,12 L3,11" id="Path-81" transform="translate(2.250000, 11.500000) scale(1, -1) translate(-2.250000, -11.500000) "></path>
13 <path d="M5,8.5 L6,7" id="Path-81" transform="translate(5.500000, 7.750000) scale(-1, 1) translate(-5.500000, -7.750000) "></path>
14 </g>
15 </g>
16 </g>
17 </g>
18</svg>
diff --git a/client/src/assets/images/video/like-grey.svg b/client/src/assets/images/video/like-grey.svg
new file mode 100644
index 000000000..5ef6c7b31
--- /dev/null
+++ b/client/src/assets/images/video/like-grey.svg
@@ -0,0 +1,15 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
4 <title>thumbs-up</title>
5 <desc>Created with Sketch.</desc>
6 <defs></defs>
7 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
8 <g id="Artboard-4" transform="translate(-708.000000, -643.000000)" stroke="#585858" stroke-width="2">
9 <g id="256" transform="translate(708.000000, 643.000000)">
10 <path d="M6,14 C6,16.5 6.5,19 8,19 L16.9938335,19 C17.5495239,19 18.1819788,18.5956028 18.4072817,18.0949295 L20.8562951,12.6526776 C21.7640882,10.6353595 20.7154925,9 18.5092545,9 L15.5,9 C15.5,9 18.5,3 15,3 C12.5,3 11.5,9 8,9 C6.5,9 6,11.5 6,14 Z" id="Path-188" stroke-linejoin="round"></path>
11 <path d="M4,8.5 C4,8.5 3,11 3,14 C3,17 4,19.5 4,19.5" id="Path-189"></path>
12 </g>
13 </g>
14 </g>
15</svg>
diff --git a/client/src/assets/images/video/like-white.svg b/client/src/assets/images/video/like-white.svg
new file mode 100644
index 000000000..88e5f6a9a
--- /dev/null
+++ b/client/src/assets/images/video/like-white.svg
@@ -0,0 +1,15 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
4 <title>thumbs-up</title>
5 <desc>Created with Sketch.</desc>
6 <defs></defs>
7 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
8 <g id="Artboard-4" transform="translate(-708.000000, -643.000000)" stroke="#ffffff" stroke-width="2">
9 <g id="256" transform="translate(708.000000, 643.000000)">
10 <path d="M6,14 C6,16.5 6.5,19 8,19 L16.9938335,19 C17.5495239,19 18.1819788,18.5956028 18.4072817,18.0949295 L20.8562951,12.6526776 C21.7640882,10.6353595 20.7154925,9 18.5092545,9 L15.5,9 C15.5,9 18.5,3 15,3 C12.5,3 11.5,9 8,9 C6.5,9 6,11.5 6,14 Z" id="Path-188" stroke-linejoin="round"></path>
11 <path d="M4,8.5 C4,8.5 3,11 3,14 C3,17 4,19.5 4,19.5" id="Path-189"></path>
12 </g>
13 </g>
14 </g>
15</svg>
diff --git a/client/src/assets/images/video/more.svg b/client/src/assets/images/video/more.svg
new file mode 100644
index 000000000..dea392136
--- /dev/null
+++ b/client/src/assets/images/video/more.svg
@@ -0,0 +1,11 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <defs></defs>
4 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
5 <g id="Artboard-4" transform="translate(-444.000000, -115.000000)" fill="#585858">
6 <g id="10" transform="translate(444.000000, 115.000000)">
7 <path d="M10,12 C10,10.8954305 10.8877296,10 12,10 C13.1045695,10 14,10.8877296 14,12 C14,13.1045695 13.1122704,14 12,14 C10.8954305,14 10,13.1122704 10,12 Z M17,12 C17,10.8954305 17.8877296,10 19,10 C20.1045695,10 21,10.8877296 21,12 C21,13.1045695 20.1122704,14 19,14 C17.8954305,14 17,13.1122704 17,12 Z M3,12 C3,10.8954305 3.88772964,10 5,10 C6.1045695,10 7,10.8877296 7,12 C7,13.1045695 6.11227036,14 5,14 C3.8954305,14 3,13.1122704 3,12 Z" id="Combined-Shape"></path>
8 </g>
9 </g>
10 </g>
11</svg>
diff --git a/client/src/assets/images/video/share.svg b/client/src/assets/images/video/share.svg
new file mode 100644
index 000000000..da0f43e81
--- /dev/null
+++ b/client/src/assets/images/video/share.svg
@@ -0,0 +1,16 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
4 <title>share</title>
5 <desc>Created with Sketch.</desc>
6 <defs></defs>
7 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
8 <g id="Artboard-4" transform="translate(-312.000000, -203.000000)" stroke="#585858" stroke-width="2">
9 <g id="47" transform="translate(312.000000, 203.000000)">
10 <path d="M20,15 L20,18.0026083 C20,19.1057373 19.1073772,20 18.0049107,20 L5.99508929,20 C4.8932319,20 4,19.1073772 4,18.0049107 L4,5.99508929 C4,4.8932319 4.89585781,4 5.9973917,4 L9,4" id="Rectangle-460"></path>
11 <polyline id="Path-93" stroke-linejoin="round" points="13 4 20.0207973 4 20.0207973 11.0191059"></polyline>
12 <path d="M19,5 L12,12" id="Path-94" stroke-linejoin="round"></path>
13 </g>
14 </g>
15 </g>
16</svg>
diff --git a/client/src/assets/images/video/upload.svg b/client/src/assets/images/video/upload.svg
new file mode 100644
index 000000000..c5b7cb443
--- /dev/null
+++ b/client/src/assets/images/video/upload.svg
@@ -0,0 +1,16 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
4 <title>cloud-upload</title>
5 <desc>Created with Sketch.</desc>
6 <defs></defs>
7 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
8 <g id="Artboard-4" transform="translate(-312.000000, -775.000000)" stroke="#C6C6C6" stroke-width="2">
9 <g id="307" transform="translate(312.000000, 775.000000)">
10 <path d="M8,18 L5,18 L5,18 C2.790861,18 1,16.209139 1,14 C1,11.790861 2.790861,10 5,10 C5.35840468,10 5.70579988,10.0471371 6.03632437,10.1355501 C6.01233106,9.92702603 6,9.71495305 6,9.5 C6,6.46243388 8.46243388,4 11.5,4 C14.0673313,4 16.2238156,5.7590449 16.8299648,8.1376465 C17.2052921,8.04765874 17.5970804,8 18,8 C20.7614237,8 23,10.2385763 23,13 C23,15.7614237 20.7614237,18 18,18 L16,18" id="Combined-Shape" stroke-linejoin="round"></path>
11 <path d="M12,13 L12,21" id="Path-58"></path>
12 <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 12.500000) scale(1, -1) translate(-12.000000, -12.500000) " points="15 11 12 14 9 11"></polyline>
13 </g>
14 </g>
15 </g>
16</svg>
diff --git a/client/src/assets/logo.png b/client/src/assets/logo.png
deleted file mode 100644
index c1d77a24c..000000000
--- a/client/src/assets/logo.png
+++ /dev/null
Binary files differ
diff --git a/client/src/assets/player/images/arrow-down.svg b/client/src/assets/player/images/arrow-down.svg
new file mode 100644
index 000000000..3377cdab2
--- /dev/null
+++ b/client/src/assets/player/images/arrow-down.svg
@@ -0,0 +1,14 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <defs></defs>
4 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
5 <g id="Artboard-4" transform="translate(-532.000000, -1046.000000)" stroke="#fff" stroke-width="2">
6 <g id="Extras" transform="translate(48.000000, 1046.000000)">
7 <g id="down" transform="translate(484.000000, 0.000000)">
8 <path d="M12,3 L12,20" id="Path-58"></path>
9 <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 17.000000) scale(-1, -1) translate(-12.000000, -17.000000) " points="4 21 12 13 20 21"></polyline>
10 </g>
11 </g>
12 </g>
13 </g>
14</svg>
diff --git a/client/src/assets/player/images/arrow-up.svg b/client/src/assets/player/images/arrow-up.svg
new file mode 100644
index 000000000..b1a7890a8
--- /dev/null
+++ b/client/src/assets/player/images/arrow-up.svg
@@ -0,0 +1,14 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <defs></defs>
4 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
5 <g id="Artboard-4" transform="translate(-488.000000, -1046.000000)" stroke="#fff" stroke-width="2">
6 <g id="Extras" transform="translate(48.000000, 1046.000000)">
7 <g id="up" transform="translate(440.000000, 0.000000)">
8 <path d="M12,4 L12,21" id="Path-58"></path>
9 <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 7.000000) scale(-1, 1) translate(-12.000000, -7.000000) " points="4 11 12 3 20 11"></polyline>
10 </g>
11 </g>
12 </g>
13 </g>
14</svg>
diff --git a/client/src/assets/player/images/fullscreen.svg b/client/src/assets/player/images/fullscreen.svg
new file mode 100644
index 000000000..44e0041a4
--- /dev/null
+++ b/client/src/assets/player/images/fullscreen.svg
@@ -0,0 +1,18 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
4 <title>fullscreen</title>
5 <desc>Created with Sketch.</desc>
6 <defs></defs>
7 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
8 <g id="Artboard-4" transform="translate(-576.000000, -159.000000)" stroke="#fff" stroke-width="2">
9 <g id="33" transform="translate(576.000000, 159.000000)">
10 <rect id="Rectangle-433" x="1" y="4" width="22" height="16" rx="1"></rect>
11 <polyline id="Path-42" stroke-linecap="round" stroke-linejoin="round" points="20 10 20 7 17 7"></polyline>
12 <polyline id="Path-42" stroke-linecap="round" stroke-linejoin="round" points="7 17 4 17 4 14"></polyline>
13 <polyline id="Path-42" stroke-linecap="round" stroke-linejoin="round" transform="translate(18.500000, 15.500000) scale(1, -1) translate(-18.500000, -15.500000) " points="20 17 20 14 17 14"></polyline>
14 <polyline id="Path-42" stroke-linecap="round" stroke-linejoin="round" transform="translate(5.500000, 8.500000) scale(1, -1) translate(-5.500000, -8.500000) " points="7 10 4 10 4 7"></polyline>
15 </g>
16 </g>
17 </g>
18</svg>
diff --git a/client/src/assets/player/images/volume-mute.svg b/client/src/assets/player/images/volume-mute.svg
new file mode 100644
index 000000000..0c7c296bc
--- /dev/null
+++ b/client/src/assets/player/images/volume-mute.svg
@@ -0,0 +1,16 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
4 <title>volume-mute</title>
5 <desc>Created with Sketch.</desc>
6 <defs></defs>
7 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
8 <g id="Artboard-4" transform="translate(-92.000000, -159.000000)" stroke="#fff" stroke-width="2">
9 <g id="22" transform="translate(92.000000, 159.000000)">
10 <path d="M2,8.99703014 C2,8.4463856 2.44335318,8 3.0093689,8 L6,8 L12,4 L12,20 L6,16 L3.0093689,16 C2.45190985,16 2,15.5469637 2,15.0029699 L2,8.99703014 Z" id="Rectangle-415" stroke-linejoin="round"></path>
11 <path d="M16,15 L22,9" id="Path-28"></path>
12 <path d="M16.0000002,15 L22.0249378,9" id="Path-28" transform="translate(19.012469, 12.000000) scale(-1, 1) translate(-19.012469, -12.000000) "></path>
13 </g>
14 </g>
15 </g>
16</svg>
diff --git a/client/src/assets/player/images/volume.svg b/client/src/assets/player/images/volume.svg
new file mode 100644
index 000000000..590913add
--- /dev/null
+++ b/client/src/assets/player/images/volume.svg
@@ -0,0 +1,13 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <defs></defs>
4 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
5 <g id="Artboard-4" transform="translate(-884.000000, -115.000000)" stroke="#fff" stroke-width="2">
6 <g id="20" transform="translate(884.000000, 115.000000)">
7 <path d="M2,8.99703014 C2,8.4463856 2.44335318,8 3.0093689,8 L6,8 L12,4 L12,20 L6,16 L3.0093689,16 C2.45190985,16 2,15.5469637 2,15.0029699 L2,8.99703014 Z" id="Rectangle-415" stroke-linejoin="round"></path>
8 <path d="M16,8 C16,8 18,9.5 18,12 C18,14.5 16,16 16,16" id="Path-26"></path>
9 <path d="M16.0734116,20 C19.3093571,18.9698098 22.0000001,15.5773201 22.0000001,12 C22.0000001,8.43619491 19.2903975,5.04132966 16.0734116,4" id="Oval-33"></path>
10 </g>
11 </g>
12 </g>
13</svg>
diff --git a/client/src/assets/player/peertube-videojs-plugin.ts b/client/src/assets/player/peertube-videojs-plugin.ts
index c54d8b5ea..4ba37b7d9 100644
--- a/client/src/assets/player/peertube-videojs-plugin.ts
+++ b/client/src/assets/player/peertube-videojs-plugin.ts
@@ -2,9 +2,24 @@
2 2
3import videojs, { Player } from 'video.js' 3import videojs, { Player } from 'video.js'
4import * as WebTorrent from 'webtorrent' 4import * as WebTorrent from 'webtorrent'
5import { VideoFile } from '../../../../shared'
5 6
6import { renderVideo } from './video-renderer' 7import { renderVideo } from './video-renderer'
7import { VideoFile } from '../../../../shared' 8
9// https://github.com/danrevah/ngx-pipes/blob/master/src/pipes/math/bytes.ts
10// Don't import all Angular stuff, just copy the code with shame
11const dictionaryBytes: Array<{max: number, type: string}> = [
12 { max: 1024, type: 'B' },
13 { max: 1048576, type: 'KB' },
14 { max: 1073741824, type: 'MB' },
15 { max: 1.0995116e12, type: 'GB' }
16]
17function bytes (value) {
18 const format = dictionaryBytes.find(d => value < d.max) || dictionaryBytes[dictionaryBytes.length - 1]
19 const calc = Math.floor(value / (format.max / 1024)).toString()
20
21 return [ calc, format.type ]
22}
8 23
9// videojs typings don't have some method we need 24// videojs typings don't have some method we need
10const videojsUntyped = videojs as any 25const videojsUntyped = videojs as any
@@ -62,6 +77,7 @@ const ResolutionMenuButton = videojsUntyped.extend(MenuButton, {
62 77
63 update: function () { 78 update: function () {
64 this.label.innerHTML = this.player_.getCurrentResolutionLabel() 79 this.label.innerHTML = this.player_.getCurrentResolutionLabel()
80 this.hide()
65 return MenuButton.prototype.update.call(this) 81 return MenuButton.prototype.update.call(this)
66 }, 82 },
67 83
@@ -74,8 +90,7 @@ MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton)
74const Button = videojsUntyped.getComponent('Button') 90const Button = videojsUntyped.getComponent('Button')
75const PeertubeLinkButton = videojsUntyped.extend(Button, { 91const PeertubeLinkButton = videojsUntyped.extend(Button, {
76 constructor: function (player) { 92 constructor: function (player) {
77 Button.apply(this, arguments) 93 Button.call(this, player)
78 this.player = player
79 }, 94 },
80 95
81 createEl: function () { 96 createEl: function () {
@@ -90,11 +105,80 @@ const PeertubeLinkButton = videojsUntyped.extend(Button, {
90 }, 105 },
91 106
92 handleClick: function () { 107 handleClick: function () {
93 this.player.pause() 108 this.player_.pause()
94 } 109 }
95}) 110})
96Button.registerComponent('PeerTubeLinkButton', PeertubeLinkButton) 111Button.registerComponent('PeerTubeLinkButton', PeertubeLinkButton)
97 112
113const WebTorrentButton = videojsUntyped.extend(Button, {
114 constructor: function (player) {
115 Button.call(this, player)
116 },
117
118 createEl: function () {
119 const div = document.createElement('div')
120 const subDiv = document.createElement('div')
121 div.appendChild(subDiv)
122
123 const downloadIcon = document.createElement('span')
124 downloadIcon.classList.add('icon', 'icon-download')
125 subDiv.appendChild(downloadIcon)
126
127 const downloadSpeedText = document.createElement('span')
128 downloadSpeedText.classList.add('download-speed-text')
129 const downloadSpeedNumber = document.createElement('span')
130 downloadSpeedNumber.classList.add('download-speed-number')
131 const downloadSpeedUnit = document.createElement('span')
132 downloadSpeedText.appendChild(downloadSpeedNumber)
133 downloadSpeedText.appendChild(downloadSpeedUnit)
134 subDiv.appendChild(downloadSpeedText)
135
136 const uploadIcon = document.createElement('span')
137 uploadIcon.classList.add('icon', 'icon-upload')
138 subDiv.appendChild(uploadIcon)
139
140 const uploadSpeedText = document.createElement('span')
141 uploadSpeedText.classList.add('upload-speed-text')
142 const uploadSpeedNumber = document.createElement('span')
143 uploadSpeedNumber.classList.add('upload-speed-number')
144 const uploadSpeedUnit = document.createElement('span')
145 uploadSpeedText.appendChild(uploadSpeedNumber)
146 uploadSpeedText.appendChild(uploadSpeedUnit)
147 subDiv.appendChild(uploadSpeedText)
148
149 const peersText = document.createElement('span')
150 peersText.textContent = ' peers'
151 peersText.classList.add('peers-text')
152 const peersNumber = document.createElement('span')
153 peersNumber.classList.add('peers-number')
154 subDiv.appendChild(peersNumber)
155 subDiv.appendChild(peersText)
156
157 div.className = 'vjs-webtorrent'
158 // Hide the stats before we get the info
159 subDiv.className = 'vjs-webtorrent-hidden'
160
161 this.player_.on('torrentInfo', (event, data) => {
162 const downloadSpeed = bytes(data.downloadSpeed)
163 const uploadSpeed = bytes(data.uploadSpeed)
164 const numPeers = data.numPeers
165
166 downloadSpeedNumber.textContent = downloadSpeed[0]
167 downloadSpeedUnit.textContent = ' ' + downloadSpeed[1]
168
169 uploadSpeedNumber.textContent = uploadSpeed[0]
170 uploadSpeedUnit.textContent = ' ' + uploadSpeed[1]
171
172 peersNumber.textContent = numPeers
173
174 subDiv.className = 'vjs-webtorrent-displayed'
175 })
176
177 return div
178 }
179})
180Button.registerComponent('WebTorrentButton', WebTorrentButton)
181
98type PeertubePluginOptions = { 182type PeertubePluginOptions = {
99 videoFiles: VideoFile[] 183 videoFiles: VideoFile[]
100 playerElement: HTMLVideoElement 184 playerElement: HTMLVideoElement
@@ -223,6 +307,12 @@ const peertubePlugin = function (options: PeertubePluginOptions) {
223 } 307 }
224 } 308 }
225 309
310 const webTorrentButton = new WebTorrentButton(player)
311 controlBar.webTorrent = controlBar.el().insertBefore(webTorrentButton.el(), controlBar.progressControl.el())
312 controlBar.webTorrent.dispose = function () {
313 this.parentNode.removeChild(this)
314 }
315
226 if (options.autoplay === true) { 316 if (options.autoplay === true) {
227 player.updateVideoFile() 317 player.updateVideoFile()
228 } else { 318 } else {
@@ -245,7 +335,7 @@ const peertubePlugin = function (options: PeertubePluginOptions) {
245 }, 1000) 335 }, 1000)
246 }) 336 })
247 337
248 function handleError (err: Error|string) { 338 function handleError (err: Error | string) {
249 return player.trigger('customError', { err }) 339 return player.trigger('customError', { err })
250 } 340 }
251} 341}
diff --git a/client/src/index.html b/client/src/index.html
index 8e94b903d..4af6b12f6 100644
--- a/client/src/index.html
+++ b/client/src/index.html
@@ -11,7 +11,7 @@
11 <!-- open graph and oembed tags --> 11 <!-- open graph and oembed tags -->
12 <!-- Do not remove it! --> 12 <!-- Do not remove it! -->
13 13
14 <link rel="icon" type="image/png" href="/client/assets/favicon.png" /> 14 <link rel="icon" type="image/png" href="/client/assets/images/favicon.png" />
15 15
16 <!-- base url --> 16 <!-- base url -->
17 <base href="<%= htmlWebpackPlugin.options.metadata.baseUrl %>"> 17 <base href="<%= htmlWebpackPlugin.options.metadata.baseUrl %>">
diff --git a/client/src/sass/_mixins.scss b/client/src/sass/_mixins.scss
new file mode 100644
index 000000000..2a7192fb2
--- /dev/null
+++ b/client/src/sass/_mixins.scss
@@ -0,0 +1,95 @@
1@mixin disable-default-a-behaviour {
2 &:hover, &:focus, &:active {
3 text-decoration: none !important;
4 outline: none !important;
5 }
6}
7
8@mixin peertube-input-text($width) {
9 display: inline-block;
10 height: $button-height;
11 width: $width;
12 background: #fff;
13 border: 1px solid #C6C6C6;
14 border-radius: 3px;
15 padding-left: 15px;
16
17 &::placeholder {
18 color: #585858;
19 }
20}
21
22@mixin orange-button {
23 color: #fff;
24 background-color: $orange-color;
25
26 &:hover, &:active, &:focus {
27 color: #fff;
28 background-color: $orange-hoover-color;
29 }
30
31 &[disabled], &.disabled {
32 cursor: default;
33 color: #fff;
34 background-color: #C6C6C6;
35 }
36}
37
38@mixin grey-button {
39 background-color: $grey-color;
40 color: #585858;
41
42 &:hover, &:active, &:focus, &[disabled], &.disabled {
43 color: #585858;
44 background-color: $grey-hoover-color;
45 }
46
47 &[disabled], &.disabled {
48 cursor: default;
49 }
50}
51
52@mixin peertube-button {
53 border: none;
54 font-weight: $font-semibold;
55 font-size: 15px;
56 height: $button-height;
57 line-height: $button-height;
58 border-radius: 3px;
59 text-align: center;
60 padding: 0 17px 0 13px;
61 cursor: pointer;
62 outline: 0;
63}
64
65@mixin peertube-button-link {
66 display: inline-block;
67
68 @include disable-default-a-behaviour;
69 @include peertube-button;
70}
71
72@mixin avatar ($size) {
73 width: $size;
74 height: $size;
75}
76
77@mixin icon ($size) {
78 display: inline-block;
79 background-repeat: no-repeat;
80 background-size: contain;
81 width: $size;
82 height: $size;
83 vertical-align: middle;
84 cursor: pointer;
85}
86
87
88@mixin peertube-select ($width) {
89 background-color: #fff;
90 border: 1px solid #C6C6C6;
91 height: $button-height;
92 width: $width;
93 border-radius: 3px;
94 padding-left: 15px;
95}
diff --git a/client/src/sass/_variables.scss b/client/src/sass/_variables.scss
index f0ffb43ba..0d310409b 100644
--- a/client/src/sass/_variables.scss
+++ b/client/src/sass/_variables.scss
@@ -1,23 +1,29 @@
1$grey-color: #555; 1$font-regular: 400;
2$font-semibold: 600;
3$font-bold: 700;
2 4
3$black-background: #1d2125; 5$grey-color: #E5E5E5;
6$grey-hoover-color: #EFEFEF;;
7$orange-color: #F1680D;
8$orange-hoover-color: #F97D46;
9
10$black-background: #000;
4$grey-background: #f6f2f2; 11$grey-background: #f6f2f2;
12$red-error: #FF0000;
13
14$expanded-horizontal-margins: 150px;
15$not-expanded-horizontal-margins: 30px;
5 16
6$menu-color-link: #9cabb8; 17$button-height: 30px;
7$menu-color-block: #686f77;
8 18
9$header-height: 65px; 19$header-height: 50px;
10$header-border-color: #e9eff6; 20$header-border-color: #e9eff6;
21$search-input-width: 375px;
22
23$menu-color: #fff;
24$menu-width: 240px;
11 25
12$footer-height: 30px; 26$footer-height: 30px;
13$footer-margin: 30px; 27$footer-margin: 30px;
14 28
15$footer-border-color: $header-border-color; 29$footer-border-color: $header-border-color;
16
17$video-miniature-other-infos: #686767;
18
19$video-watch-border-color: #eceef4;
20$video-watch-title-height: 90px;
21$video-watch-info-color: #9da0ae;
22$video-watch-info-height: 120px;
23$video-watch-info-padding-left: 40px;
diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss
index 47e1b6df0..9d347d566 100644
--- a/client/src/sass/application.scss
+++ b/client/src/sass/application.scss
@@ -1,3 +1,5 @@
1$FontPathSourceSansPro: "../fonts/source-sans-pro";
2@import '~source-sans-pro/source-sans-pro';
1@import '~primeng/resources/themes/bootstrap/theme.css'; 3@import '~primeng/resources/themes/bootstrap/theme.css';
2@import '~primeng/resources/primeng.css'; 4@import '~primeng/resources/primeng.css';
3@import '~video.js/dist/video-js.css'; 5@import '~video.js/dist/video-js.css';
@@ -7,17 +9,30 @@
7 display: none !important; 9 display: none !important;
8} 10}
9 11
12body {
13 font-family: 'Source Sans Pro';
14 font-weight: $font-regular;
15 color: #000;
16}
17
10input.readonly { 18input.readonly {
11 /* Force blank on readonly inputs */ 19 /* Force blank on readonly inputs */
12 background-color: #fff !important; 20 background-color: #fff !important;
13} 21}
14 22
15.form-control, .btn { 23label {
16 border-radius: 0; 24 font-weight: $font-bold;
25 font-size: 15px;
17} 26}
18 27
19.dropdown-menu { 28.form-error {
20 border-radius: 0; 29 display: block;
30 color: $red-error;
31 margin-top: 5px;
32}
33
34.input-error {
35 border-color: $red-error !important;
21} 36}
22 37
23.glyphicon-black { 38.glyphicon-black {
@@ -25,44 +40,73 @@ input.readonly {
25} 40}
26 41
27.main-col { 42.main-col {
28 .content-padding { 43 margin-left: $menu-width;
29 padding: 15px 30px;
30 44
31 @media screen and (max-width: 800px) { 45 .margin-content {
32 padding: 15px 10px; 46 margin-left: $not-expanded-horizontal-margins;
33 } 47 margin-right: $not-expanded-horizontal-margins;
48 }
34 49
35 @media screen and (min-width: 1400px) { 50 .sub-menu {
36 padding: 15px 40px; 51 background-color: #F7F7F7;
37 } 52 width: 100%;
53 height: 81px;
54 margin-bottom: 30px;
55 display: flex;
56 align-items: center;
57 padding-left: $not-expanded-horizontal-margins;
58 }
38 59
39 @media screen and (min-width: 1600px) { 60 // Override some properties if the main content is expanded (no menu on the left)
40 padding: 15px 50px; 61 &.expanded {
62 margin-left: 0;
63
64 .margin-content {
65 margin-left: $expanded-horizontal-margins;
66 margin-right: $expanded-horizontal-margins;
41 } 67 }
42 68
43 @media screen and (min-width: 1800px) { 69 .sub-menu {
44 padding: 15px 60px; 70 padding-left: $expanded-horizontal-margins;
45 } 71 }
46 } 72 }
47} 73}
48 74
49// On small screen, menu is absolute and displayed over the page 75.title-page {
50@media screen and (max-width: 500px) { 76 color: #000;
51 .title-menu-left { 77 font-size: 16px;
52 width: 120px; 78 display: inline-block;
53 position: absolute !important; 79 margin-right: 55px;
54 z-index: 10000; 80 font-weight: $font-semibold;
81 @include disable-default-a-behaviour;
82
83 &.active, &.title-page-single {
84 border-bottom: 2px solid $orange-color;
85 font-weight: $font-bold;
86 margin-top: 30px;
87 margin-bottom: 25px;
55 } 88 }
56 89
57 .main-col { 90 &:hover, &:active, &:focus {
58 width: 100% !important; 91 color: #000;
59 } 92 }
93}
94
95.admin-sub-header {
96 display: flex;
97 align-items: center;
98 margin-bottom: 30px;
60 99
61 .fake-menu { 100 .admin-sub-title {
62 display: none; 101 flex-grow: 1;
63 } 102 }
64} 103}
65 104
105.admin-sub-title {
106 font-size: 20px;
107 font-weight: bold;
108}
109
66// Thanks https://gist.github.com/alexandrevicenzi/680147013e902a4eaa5d 110// Thanks https://gist.github.com/alexandrevicenzi/680147013e902a4eaa5d
67.glyphicon-refresh-animate { 111.glyphicon-refresh-animate {
68 -animation: spin .7s infinite linear; 112 -animation: spin .7s infinite linear;
@@ -86,13 +130,209 @@ input.readonly {
86 to { -moz-transform: rotate(360deg);} 130 to { -moz-transform: rotate(360deg);}
87} 131}
88 132
89/* ngprime data table customizations */ 133// ngprime data table customizations
90p-datatable { 134p-datatable {
135 font-size: 15px !important;
136
137 .ui-datatable-scrollable-header {
138 background-color: #fff !important;
139 }
140
141 .ui-widget-content {
142 border: none !important;
143 }
144
145 .ui-datatable-virtual-table {
146 border-top: none !important;
147 }
148
149 td {
150 border: 1px solid #E5E5E5 !important;
151 padding-left: 15px !important;
152 }
153
154 tr {
155 background-color: #fff !important;
156 height: 46px;
157
158 &:hover {
159 background-color: #f0f0f0 !important;
160 }
161
162 &:not(:hover) {
163 .action-cell * {
164 display: none !important;
165 }
166 }
167
168 &:first-child td {
169 border-top: none !important;
170 }
171
172 &:last-child td {
173 border-bottom: none !important;
174 }
175 }
176
177 th {
178 border: none !important;
179 border-bottom: 1px solid #f0f0f0 !important;
180 text-align: left !important;
181 padding: 5px 0 5px 15px !important;
182 font-weight: $font-semibold !important;
183 color: #000 !important;
184
185 &.ui-sortable-column:hover:not(.ui-state-active) {
186 background-color: #f0f0f0 !important;
187 border: 1px solid #f0f0f0 !important;
188 border-width: 0 1px !important;
189 }
190
191 &.ui-state-active {
192 color: #fff !important;
193 background-color: $orange-color !important;
194 border: 1px solid $orange-color !important;
195 border-width: 0 1px !important;
196 }
197 }
198
91 .action-cell { 199 .action-cell {
200 width: 250px !important;
201 padding: 0 !important;
92 text-align: center; 202 text-align: center;
203 }
204
205 p-paginator {
206 .ui-paginator-bottom {
207 position: relative;
208 border: none !important;
209 border: 1px solid #f0f0f0 !important;
210 height: 40px;
211 display: flex;
212 justify-content: center;
213 align-items: center;
214
215 a {
216 color: #000 !important;
217 font-weight: $font-semibold !important;
218 margin-right: 20px !important;
219 outline: 0 !important;
220 border-radius: 3px !important;
221 padding: 5px 2px !important;
222
223 &.ui-state-active {
224 &, &:hover, &:active, &:focus {
225 color: #fff !important;
226 background-color: $orange-color !important;
227 }
228 }
229 }
230 }
231 }
232}
233
234// Bootstrap customizations
235.dropdown-menu {
236 border-radius: 3px;
237 box-shadow: 0 3px 6px;
238 font-size: 15px;
239
240 .dropdown-item {
241 padding: 3px 15px;
242 }
243
244 a {
245 color: #000 !important;
246 }
247}
248
249.modal {
250 .modal-header {
251 border-bottom: none;
252
253 .title-page-single {
254 margin: 0;
255 }
256 }
257}
258
259.nav {
260 font-size: 16px !important;
261 border: none !important;
262
263 .nav-item .nav-link {
264 margin-right: 30px;
265 padding: 0;
266 border-radius: 3px;
267 border: none !important;
268
269 .tab-link {
270 display: flex !important;
271 align-items: center;
272 height: 30px !important;
273 padding: 0 15px;
274 }
275
276 &, & a {
277 color: #000 !important;
278 @include disable-default-a-behaviour;
279 }
280
281 &.active, &:hover {
282 background-color: #F0F0F0;
283 }
284
285 &.active {
286 font-weight: $font-semibold !important;
287 }
288 }
289}
290
291.orange-button {
292 @include peertube-button;
293 @include orange-button;
294}
295
296.orange-button-link {
297 @include peertube-button-link;
298 @include orange-button;
299}
300
301.grey-button {
302 @include peertube-button;
303 @include grey-button;
304}
305
306.grey-button-link {
307 @include peertube-button-link;
308 @include grey-button;
309}
310
311// On small screen, menu is absolute
312@media screen and (max-width: 800px) {
313 .title-menu-left {
314 width: 150px !important;
315 position: absolute !important;
316 z-index: 10000;
317 }
318
319 .main-col {
320 margin-left: 0;
321
322 &, &.expanded {
323 .margin-content {
324 margin-left: 10px;
325 margin-right: 10px;
326 }
327
328 .sub-menu {
329 padding-left: 10px;
330 margin-bottom: 10px;
331 }
93 332
94 .glyphicon { 333 input[type=text], input[type=password] {
95 cursor: pointer; 334 width: 100% !important;
335 }
96 } 336 }
97 } 337 }
98} 338}
diff --git a/client/src/sass/pre-customizations.scss b/client/src/sass/pre-customizations.scss
index 693489828..52eef50f2 100644
--- a/client/src/sass/pre-customizations.scss
+++ b/client/src/sass/pre-customizations.scss
@@ -1,4 +1,5 @@
1@import '_variables.scss'; 1@import '_variables.scss';
2@import '_mixins.scss';
2 3
3$bootstrap-sass-asset-helper: false !default; 4$bootstrap-sass-asset-helper: false !default;
4// 5//
diff --git a/client/src/sass/video-js-custom.scss b/client/src/sass/video-js-custom.scss
index 34a958764..1c5701bea 100644
--- a/client/src/sass/video-js-custom.scss
+++ b/client/src/sass/video-js-custom.scss
@@ -1,346 +1,322 @@
1// Thanks: https://github.com/kmoskwiak/videojs-resolution-switcher/pull/92/files 1// Thanks: https://github.com/zanechua/videojs-sublime-inspired-skin
2.vjs-resolution-button-label { 2$primary-foreground-color: #fff;
3 font-size: 1em; 3$primary-background-color: #000;
4 line-height: 3em; 4$font-size: 13px;
5 position: absolute; 5$control-bar-height: 34px;
6 top: 0;
7 left: -1px;
8 width: 100%;
9 height: 100%;
10 text-align: center;
11 box-sizing: inherit;
12}
13
14.vjs-resolution-button {
15 outline: 0 !important;
16 6
17 .vjs-menu { 7.video-js.vjs-peertube-skin {
18 .vjs-menu-content { 8 font-size: $font-size;
19 width: 4em; 9 color: $primary-foreground-color;
20 left: 50%; /* Center the menu, in it's parent */
21 margin-left: -2em; /* half of width, to center */
22 }
23 10
24 li { 11 .vjs-button > .vjs-icon-placeholder::before {
25 text-transform: none; 12 line-height: $control-bar-height;
26 font-size: 1em;
27 }
28 } 13 }
29}
30 14
31// Thanks: https://github.com/zanechua/videojs-sublime-inspired-skin 15 .vjs-mouse-display:before,
32 16 .vjs-play-progress:before,
33// Video JS Sublime Skin 17 .vjs-volume-level:before {
34// The following are SCSS variables to automate some of the values. 18 content: ''; /* Remove Circle From Progress Bar */
35// But don't feel limited by them. Change/replace whatever you want. 19 }
36
37// The color of icons, text, and the big play button border.
38// Try changing to #0f0
39$primary-foreground-color: #fff; // #fff default
40 20
41// The default color of control backgrounds is mostly black but with a little 21 .vjs-audio-button {
42// bit of blue so it can still be seen on all-black video frames, which are common. 22 display: none;
43// Try changing to #900 23 }
44$primary-background-color: #2B333F; // #2B333F default
45 24
46// Try changing to true 25 .vjs-big-play-button {
47$center-big-play-button: true; // true default 26 outline: 0;
27 font-size: 8em;
48 28
49.video-js { 29 $big-play-width: 3em;
50 /* The base font size controls the size of everything, not just text. 30 $big-play-height: 1.5em;
51 All dimensions use em-based sizes so that the scale along with the font size.
52 Try increasing it to 15px and see what happens. */
53 font-size: 10px;
54 31
55 /* The main font color changes the ICON COLORS as well as the text */ 32 border: 0;
56 color: $primary-foreground-color; 33 border-radius: 0.3em;
57}
58 34
59/* The "Big Play Button" is the play button that shows before the video plays.
60 To center it set the align values to center and middle. The typical location
61 of the button is the center, but there is trend towards moving it to a corner
62 where it gets out of the way of valuable content in the poster image.*/
63.vjs-sublime-skin .vjs-big-play-button {
64 /* The font size is what makes the big play button...big.
65 All width/height values use ems, which are a multiple of the font size.
66 If the .video-js font-size is 10px, then 3em equals 30px.*/
67 font-size: 8em;
68
69 /* We're using SCSS vars here because the values are used in multiple places.
70 Now that font size is set, the following em values will be a multiple of the
71 new font size. If the font-size is 3em (30px), then setting any of
72 the following values to 3em would equal 30px. 3 * font-size. */
73 $big-play-width: 3em;
74 /* 1.5em = 45px default */
75 $big-play-height: 1.5em;
76
77 line-height: $big-play-height;
78 height: $big-play-height;
79 width: $big-play-width;
80
81 /* 0.06666em = 2px default */
82 border: 0;
83 /* 0.3em = 9px default */
84 border-radius: 0.3em;
85
86 @if $center-big-play-button {
87 /* Align center */
88 left: 50%; 35 left: 50%;
89 top: 50%; 36 top: 50%;
90 margin-left: -($big-play-width / 2); 37 margin-left: -($big-play-width / 2);
91 margin-top: -($big-play-height / 2); 38 margin-top: -($big-play-height / 2);
92 } @else { 39 background-color: transparent !important;
93 /* Align top left. 0.5em = 15px default */
94 left: 0.5em;
95 top: 0.5em;
96 } 40 }
97}
98 41
99/* The default color of control backgrounds is mostly black but with a little 42 .vjs-control-bar,
100 bit of blue so it can still be seen on all-black video frames, which are common. */ 43 .vjs-big-play-button,
101.video-js .vjs-control-bar, 44 .vjs-menu-button .vjs-menu-content {
102.video-js .vjs-big-play-button, 45 background-color: rgba($primary-background-color, 0.5);
103.video-js .vjs-menu-button .vjs-menu-content { 46 }
104 /* IE8 - has no alpha support */
105 background-color: $primary-background-color;
106 /* Opacity: 1.0 = 100%, 0.0 = 0% */
107 background-color: rgba($primary-background-color, 0.7);
108 background-color: transparent;
109}
110 47
111// Make a slightly lighter version of the main background 48 $slider-bg-color: lighten($primary-background-color, 33%);
112// for the slider background.
113$slider-bg-color: lighten($primary-background-color, 33%);
114
115/* Slider - used for Volume bar and Progress bar */
116.video-js .vjs-slider {
117 background-color: $slider-bg-color;
118 background-color: rgba($slider-bg-color, 0.5);
119 background-color: rgba(255,255,255,.3);
120 border-radius: 2px;
121 height: 6.5px;
122}
123 49
124/* The slider bar color is used for the progress bar and the volume bar 50 .vjs-slider {
125 (the first two can be removed after a fix that's coming) */ 51 background-color: rgba(255, 255, 255, .3);
126.video-js .vjs-volume-level, 52 border-radius: 2px;
127.video-js .vjs-play-progress, 53 height: 5px;
128.video-js .vjs-slider-bar { 54 }
129 background: $primary-foreground-color;
130}
131 55
132/* Enlarged Slider to enable easier tracking. Adjust all the height:6.5px to preferred height for the slider if necessary. */ 56 /* The slider bar color is used for the progress bar and the volume bar
133.video-js .vjs-progress-holder .vjs-load-progress, 57 (the first two can be removed after a fix that's coming) */
134.video-js .vjs-progress-holder .vjs-load-progress div, 58 .vjs-volume-level,
135.video-js .vjs-progress-holder .vjs-play-progress, 59 .vjs-play-progress,
136.video-js .vjs-progress-holder .vjs-tooltip-progress-bar { 60 .vjs-slider-bar {
137 height: 6.5px; 61 background: $primary-foreground-color;
138} 62 }
139 63
140/* The main progress bar also has a bar that shows how much has been loaded. */ 64 .vjs-load-progress {
141.video-js .vjs-load-progress { 65 background: rgba($slider-bg-color, 0.5);
142 /* For IE8 we'll lighten the color */ 66 }
143 background: ligthen($slider-bg-color, 25%);
144 /* Otherwise we'll rely on stacked opacities */
145 background: rgba($slider-bg-color, 0.5);
146}
147 67
148/* The load progress bar also has internal divs that represent 68 .vjs-load-progress div {
149 smaller disconnected loaded time ranges */ 69 background: rgba($slider-bg-color, 0.75);
150.video-js .vjs-load-progress div { 70 }
151 /* For IE8 we'll lighten the color */
152 background: ligthen($slider-bg-color, 50%);
153 /* Otherwise we'll rely on stacked opacities */
154 background: rgba($slider-bg-color, 0.75);
155}
156 71
157//Skin Style Starts 72 .vjs-poster {
158.vjs-sublime-skin .vjs-poster {
159 outline: none; /* Remove Blue Outline on Click*/ 73 outline: none; /* Remove Blue Outline on Click*/
160 outline: 0; 74 outline: 0;
161} 75 }
162
163.vjs-sublime-skin:hover .vjs-big-play-button {
164 background-color: transparent;
165}
166
167.vjs-sublime-skin .vjs-fullscreen-control:before,
168.vjs-sublime-skin.vjs-fullscreen .vjs-fullscreen-control:before {
169 content: ''; /* Remove Fullscreen Exit Icon */
170}
171
172.vjs-sublime-skin.vjs-fullscreen .vjs-fullscreen-control {
173 background: #fff;
174}
175
176.vjs-sublime-skin .vjs-fullscreen-control {
177 border: 3px solid #fff;
178 box-sizing: border-box;
179 cursor: pointer;
180 margin-top: -7px;
181 top: 50%;
182 height: 14px;
183 width: 22px;
184 margin-right: 10px;
185}
186
187.vjs-sublime-skin.vjs-fullscreen .vjs-fullscreen-control:after {
188 background: #000;
189 content: "";
190 display: block;
191 position: absolute;
192 bottom: 0;
193 left: 0;
194 height: 5px;
195 width: 5px;
196}
197
198.vjs-sublime-skin .vjs-progress-holder {
199 margin: 0;
200}
201
202.vjs-sublime-skin .vjs-progress-control .vjs-progress-holder:after {
203 border-radius: 2px;
204 display: block;
205 height: 6.5px;
206}
207
208.vjs-sublime-skin .vjs-progress-control .vjs-load-progres,
209.vjs-sublime-skin .vjs-progress-control .vjs-play-progress {
210 border-radius: 2px;
211 height: 6.5px;
212}
213
214.vjs-sublime-skin .vjs-playback-rate {
215 display: none; /* Remove Playback Rate */
216}
217
218.vjs-sublime-skin .vjs-progress-control {
219 margin-right: 50px;
220}
221
222.vjs-sublime-skin .vjs-time-control {
223 right: 55px;
224}
225 76
226.vjs-sublime-skin .vjs-volume-menu-button:before { 77 .vjs-control-bar {
227 width: 1.2em; 78 height: $control-bar-height;
228 z-index: 1;
229}
230 79
231.vjs-sublime-skin .vjs-volume-menu-button .vjs-menu, 80 .vjs-progress-control {
232.vjs-sublime-skin .vjs-volume-menu-button:focus .vjs-menu, 81 bottom: 34px;
233.vjs-sublime-skin .vjs-volume-menu-button.vjs-slider-active .vjs-menu { 82 width: 100%;
234 display: block; 83 position: absolute;
235 opacity: 1; 84 height: 5px;
236}
237 85
238.vjs-sublime-skin .vjs-volume-menu-button, 86 .vjs-progress-holder {
239.vjs-sublime-skin .vjs-volume-panel { 87 margin: 0;
240 width: 6em; 88 border-radius: 0;
241 position: absolute; 89 }
242 right: 0; 90 }
243 margin-right: 65px;
244}
245 91
246.vjs-sublime-skin .vjs-volume-menu-button .vjs-menu-content, 92 .vjs-play-control {
247.vjs-sublime-skin .vjs-volume-menu-button:hover, 93 font-size: $font-size;
248.vjs-sublime-skin .vjs-volume-menu-button:focus, 94 padding: 0 17px;
249.vjs-sublime-skin .vjs-volume-menu-button.vjs-slider-active, 95 margin-right: 5px;
250.vjs-sublime-skin .vjs-volume-panel .vjs-volume-control, 96 }
251.vjs-sublime-skin .vjs-volume-panel:hover,
252.vjs-sublime-skin .vjs-volume-panel:focus,
253.vjs-sublime-skin .vjs-volume-panel.vjs-slider-active {
254 width: 6em;
255}
256 97
257.vjs-sublime-skin .vjs-volume-menu-button .vjs-menu { 98 .vjs-time-control {
258 left: 23px; 99 &.vjs-current-time {
259} 100 font-size: $font-size;
101 display: inline-block;
102 padding: 0;
103
104 .vjs-current-time-display {
105 line-height: $control-bar-height;
106
107 &::after {
108 content: "/";
109 margin: 0 1px 0 2px;
110 }
111 }
112 }
113
114 &.vjs-duration {
115 font-size: $font-size;
116 display: inline-block;
117 padding: 0;
118
119 .vjs-duration-display {
120 line-height: $control-bar-height;
121 }
122 }
123
124 &.vjs-remaining-time {
125 display: none;
126 }
127 }
260 128
261.vjs-sublime-skin .vjs-mouse-display:before, 129 .vjs-webtorrent {
262.vjs-sublime-skin .vjs-play-progress:before, 130 width: 100%;
263.vjs-sublime-skin .vjs-volume-level:before { 131 line-height: $control-bar-height;
264 content: ''; /* Remove Circle From Progress Bar */ 132 text-align: right;
265} 133 padding-right: 60px;
134
135 .vjs-webtorrent-displayed {
136 display: block;
137 }
138
139 .vjs-webtorrent-hidden {
140 display: none;
141 }
142
143 .download-speed-number, .upload-speed-number, .peers-number {
144 font-weight: $font-semibold;
145 }
146
147 .download-speed-text, .upload-speed-text, .peers-text {
148 margin-right: 15px;
149 }
150
151 .icon {
152 display: inline-block;
153 width: 15px;
154 height: 15px;
155 background-size: contain;
156 vertical-align: middle;
157 background-repeat: no-repeat;
158 margin-right: 6px;
159 position: relative;
160 top: -1px;
161
162 &.icon-download {
163 background-image: url('../assets/player/images/arrow-down.svg');
164 }
165
166 &.icon-upload {
167 background-image: url('../assets/player/images/arrow-up.svg');
168 }
169 }
170 }
266 171
267.vjs-sublime-skin .vjs-mouse-display:after, 172 .vjs-mute-control {
268.vjs-sublime-skin .vjs-play-progress:after, 173 .vjs-icon-placeholder {
269.vjs-sublime-skin .vjs-time-tooltip { 174 display: inline-block;
270 width: 5.5em; 175 width: 22px;
271} 176 height: 22px;
177 vertical-align: middle;
178 background: url('../assets/player/images/volume.svg') no-repeat;
179 background-size: contain;
180
181 &::before {
182 content: '';
183 }
184 }
185
186 &.vjs-vol-0 .vjs-icon-placeholder {
187 background: url('../assets/player/images/volume-mute.svg') no-repeat;
188 background-size: contain;
189 }
190 }
272 191
273.vjs-sublime-skin .vjs-audio-button { 192 .vjs-volume-menu-button,
274 display: none; 193 .vjs-volume-panel {
275} 194 width: 6em;
195 position: absolute;
196 right: 0;
197 margin-right: 65px;
198 }
276 199
277.vjs-sublime-skin .vjs-volume-bar { 200 .vjs-volume-bar {
278 background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAcCAQAAACw95UnAAAAMElEQVRIx2NgoBL4n4YKGUYNHkEG4zJg1OCRYDCpBowaPJwMppbLRg0eNXjUYBLEAXWNUA6QNm1lAAAAAElFTkSuQmCC); 201 background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAcCAQAAACw95UnAAAAMElEQVRIx2NgoBL4n4YKGUYNHkEG4zJg1OCRYDCpBowaPJwMppbLRg0eNXjUYBLEAXWNUA6QNm1lAAAAAElFTkSuQmCC) no-repeat;
279 background-size: 22px 14px; 202 background-size: 22px 14px;
280 background-repeat: no-repeat; 203 height: 100%;
281 height: 100%; 204 width: 100%;
282 width: 100%; 205 max-width: 22px;
283 max-width: 22px; 206 max-height: 14px;
284 max-height: 14px; 207 margin: 7px 4px;
285 margin: 7px 4px; 208 border-radius: 0;
286 border-radius: 0; 209 top: 3px;
287} 210
211 .vjs-volume-level {
212 background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAcAQAAAAAyhWABAAAAAnRSTlMAAHaTzTgAAAAZSURBVHgBYwAB/g9EUv+JokCqiaT+U4MCAPKPS7WUUOc1AAAAAElFTkSuQmCC) no-repeat;
213 background-size: 22px 14px;
214 max-width: 22px;
215 max-height: 14px;
216 height: 100%;
217 }
218 }
288 219
289.vjs-sublime-skin .vjs-volume-level { 220 .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active,
290 background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAcAQAAAAAyhWABAAAAAnRSTlMAAHaTzTgAAAAZSURBVHgBYwAB/g9EUv+JokCqiaT+U4MCAPKPS7WUUOc1AAAAAElFTkSuQmCC); 221 .vjs-volume-panel.vjs-volume-panel-horizontal:active,
291 background-size: 22px 14px; 222 .vjs-volume-panel.vjs-volume-panel-horizontal:focus,
292 background-repeat: no-repeat; 223 .vjs-volume-panel.vjs-volume-panel-horizontal:hover {
293 max-width: 22px; 224 width: 6em;
294 max-height: 14px; 225 transition-property: none;
295 height: 100%; 226 }
296}
297 227
298/* New for VideoJS v6 */ 228 .vjs-volume-panel .vjs-mute-control:hover ~ .vjs-volume-control.vjs-volume-horizontal {
299.vjs-sublime-skin .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active, 229 width: 3em;
300.vjs-sublime-skin .vjs-volume-panel.vjs-volume-panel-horizontal:active, 230 height: auto;
301.vjs-sublime-skin .vjs-volume-panel.vjs-volume-panel-horizontal:focus, 231 }
302.vjs-sublime-skin .vjs-volume-panel.vjs-volume-panel-horizontal:hover {
303 width: 6em;
304 transition-property: none;
305}
306 232
307.vjs-sublime-skin .vjs-volume-panel .vjs-mute-control:hover ~ .vjs-volume-control.vjs-volume-horizontal { 233 .vjs-volume-panel .vjs-mute-control:hover ~ .vjs-volume-control {
308 width: 3em; 234 transition-property: none;
309 height: auto; 235 }
310}
311 236
312.vjs-sublime-skin .vjs-volume-panel .vjs-mute-control:hover ~ .vjs-volume-control { 237 .vjs-volume-panel {
313 transition-property: none; 238 .vjs-mute-control {
314} 239 width: 2em;
240 z-index: 1;
241 padding: 0;
242 }
243
244 .vjs-volume-control {
245 display: inline-block;
246 position: relative;
247 left: 5px;
248 opacity: 1;
249 width: 3em;
250 height: auto;
251 }
252 }
315 253
316.vjs-sublime-skin .vjs-fullscreen-control .vjs-icon-placeholder { 254 .vjs-fullscreen-control {
317 display: none; /* Remove Duplicate Fullscreen Icon */ 255 width: 37px;
318} 256
257 .vjs-icon-placeholder {
258 display: inline-block;
259 width: 22px;
260 height: 22px;
261 vertical-align: middle;
262 background: url('../assets/player/images/fullscreen.svg') no-repeat;
263 background-size: contain;
264
265 &::before {
266 content: '';
267 }
268 }
269 }
319 270
320.vjs-sublime-skin .vjs-volume-panel .vjs-mute-control { 271 .vjs-menu-button-popup {
321 width: 2em; 272 font-size: 13px;
322 z-index: 1; 273 font-weight: $font-semibold;
323 padding: 0; 274 width: 42px;
324} 275
276 // Thanks: https://github.com/kmoskwiak/videojs-resolution-switcher/pull/92/files
277 .vjs-resolution-button-label {
278 line-height: $control-bar-height;
279 position: absolute;
280 top: 0;
281 left: -1px;
282 width: 100%;
283 height: 100%;
284 text-align: center;
285 box-sizing: inherit;
286 }
287
288 .vjs-resolution-button {
289 outline: 0 !important;
290 }
291
292 .vjs-menu {
293 top: 20px;
294
295 .vjs-menu-content {
296 width: 4em;
297 left: 50%; /* Center the menu, in it's parent */
298 margin-left: -2em; /* half of width, to center */
299 }
300
301 li {
302 text-transform: none;
303 font-size: 13px;
304 }
305 }
306 }
307 }
325 308
326.vjs-sublime-skin .vjs-volume-panel .vjs-volume-control { 309 @media screen and (max-width: 450px) {
327 display: inline-block; 310 .vjs-webtorrent-displayed {
328 position: relative; 311 display: none !important;
329 left: 5px; 312 }
330 opacity: 1; 313 }
331 width: 3em;
332 height: auto;
333} 314}
334 315
335// Thanks: https://projects.lukehaas.me/css-loaders/ 316// Thanks: https://projects.lukehaas.me/css-loaders/
336.vjs-loading-spinner { 317.vjs-loading-spinner {
337 margin: 0 !important;
338 position: absolute;
339 // 15px is the nav bar height
340 top: calc(50% - 15px);
341 left: 50%; 318 left: 50%;
342 font-size: 10px; 319 font-size: 10px;
343 position: relative;
344 text-indent: -9999em; 320 text-indent: -9999em;
345 border: 0.7em solid rgba(255, 255, 255, 0.2); 321 border: 0.7em solid rgba(255, 255, 255, 0.2);
346 border-left-color: #ffffff; 322 border-left-color: #ffffff;
@@ -367,3 +343,4 @@ $slider-bg-color: lighten($primary-background-color, 33%);
367 } 343 }
368 } 344 }
369} 345}
346
diff --git a/client/src/standalone/videos/embed.html b/client/src/standalone/videos/embed.html
index 0a35bc362..fa4d0bdba 100644
--- a/client/src/standalone/videos/embed.html
+++ b/client/src/standalone/videos/embed.html
@@ -11,7 +11,7 @@
11 11
12 <body> 12 <body>
13 13
14 <video id="video-container" class="video-js vjs-sublime-skin vjs-big-play-centered"> 14 <video id="video-container" class="video-js vjs-peertube-skin">
15 </video> 15 </video>
16 16
17 </body> 17 </body>
diff --git a/client/src/standalone/videos/embed.scss b/client/src/standalone/videos/embed.scss
index b76f09677..9140cd37c 100644
--- a/client/src/standalone/videos/embed.scss
+++ b/client/src/standalone/videos/embed.scss
@@ -23,17 +23,13 @@ html, body {
23} 23}
24 24
25.vjs-peertube-link { 25.vjs-peertube-link {
26 color: white; 26 color: #fff;
27 text-decoration: none; 27 text-decoration: none;
28 font-size: 1.3em; 28 font-size: $font-size;
29 line-height: 2.20; 29 line-height: $control-bar-height;
30 transition: all .4s; 30 transition: all .4s;
31 position: relative; 31 font-weight: $font-semibold;
32 right: 8px; 32 margin-right: 3px;
33}
34
35.vjs-resolution-button-label {
36 left: -7px;
37} 33}
38 34
39.vjs-peertube-link:hover { 35.vjs-peertube-link:hover {
@@ -42,5 +38,21 @@ html, body {
42 38
43// Fix volume panel because we added a new component (PeerTube link) 39// Fix volume panel because we added a new component (PeerTube link)
44.vjs-volume-panel { 40.vjs-volume-panel {
45 margin-right: 130px !important; 41 margin-right: 121px !important;
42}
43
44@media screen and (max-width: 350px) {
45 .vjs-play-control {
46 padding: 0 5px !important;
47 width: 25px !important;
48 }
49
50 .vjs-volume-control {
51 display: none !important;
52 }
53
54 .vjs-volume-panel {
55 width: 26px !important;
56 margin-right: 140px !important;
57 }
46} 58}
diff --git a/client/yarn.lock b/client/yarn.lock
index c5a47bb89..bd6870061 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -264,10 +264,6 @@ amdefine@>=0.0.4:
264 version "1.0.1" 264 version "1.0.1"
265 resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" 265 resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
266 266
267angular-pipes@^6.0.0:
268 version "6.5.3"
269 resolved "https://registry.yarnpkg.com/angular-pipes/-/angular-pipes-6.5.3.tgz#6bed37c51ebc2adaf3412663bfe25179d0489b02"
270
271angular2-notifications@^0.7.7: 267angular2-notifications@^0.7.7:
272 version "0.7.8" 268 version "0.7.8"
273 resolved "https://registry.yarnpkg.com/angular2-notifications/-/angular2-notifications-0.7.8.tgz#ecbcb95a8d2d402af94a9a080d6664c70d33a029" 269 resolved "https://registry.yarnpkg.com/angular2-notifications/-/angular2-notifications-0.7.8.tgz#ecbcb95a8d2d402af94a9a080d6664c70d33a029"
@@ -4708,9 +4704,9 @@ ngc-webpack@3.2.2:
4708 source-map "^0.5.6" 4704 source-map "^0.5.6"
4709 ts-node "^3.2.0" 4705 ts-node "^3.2.0"
4710 4706
4711ngx-bootstrap@1.9.3: 4707ngx-bootstrap@2.0.0-beta.9:
4712 version "1.9.3" 4708 version "2.0.0-beta.9"
4713 resolved "https://registry.yarnpkg.com/ngx-bootstrap/-/ngx-bootstrap-1.9.3.tgz#28e75d14fb1beaee609383d7694de4eb3ba03b26" 4709 resolved "https://registry.yarnpkg.com/ngx-bootstrap/-/ngx-bootstrap-2.0.0-beta.9.tgz#9aa7c88269534e7a5440481f31b137549f749796"
4714 4710
4715ngx-chips@1.5.3: 4711ngx-chips@1.5.3:
4716 version "1.5.3" 4712 version "1.5.3"
@@ -4718,6 +4714,14 @@ ngx-chips@1.5.3:
4718 dependencies: 4714 dependencies:
4719 ng2-material-dropdown "0.7.10" 4715 ng2-material-dropdown "0.7.10"
4720 4716
4717ngx-infinite-scroll@^0.7.0:
4718 version "0.7.0"
4719 resolved "https://registry.yarnpkg.com/ngx-infinite-scroll/-/ngx-infinite-scroll-0.7.0.tgz#a390c61c6a05ac14485e1c5bc8b4e6f6bd62fd6a"
4720
4721ngx-pipes@^2.0.5:
4722 version "2.0.5"
4723 resolved "https://registry.yarnpkg.com/ngx-pipes/-/ngx-pipes-2.0.5.tgz#743b827e350b1e66f5bdae49e90a02fa631d4c54"
4724
4721no-case@^2.2.0: 4725no-case@^2.2.0:
4722 version "2.3.2" 4726 version "2.3.2"
4723 resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" 4727 resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac"
@@ -6602,6 +6606,10 @@ source-map@^0.6.1, source-map@~0.6.1:
6602 version "0.6.1" 6606 version "0.6.1"
6603 resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 6607 resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
6604 6608
6609source-sans-pro@^2.0.10:
6610 version "2.0.10"
6611 resolved "https://registry.yarnpkg.com/source-sans-pro/-/source-sans-pro-2.0.10.tgz#c1ca859cf164a088944c5e83745085e87cd533a9"
6612
6605spdx-correct@~1.0.0: 6613spdx-correct@~1.0.0:
6606 version "1.0.2" 6614 version "1.0.2"
6607 resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" 6615 resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"