aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <florian.bigard@gmail.com>2017-12-11 11:06:32 +0100
committerChocobozzz <florian.bigard@gmail.com>2017-12-11 11:06:32 +0100
commitfada8d75550dc7365f7e18ee1569b9406251d660 (patch)
treedb9dc01c18693824f83fce5020f4c1f3ae7c0865
parent492fd28167f770d79a430fc57451b5a9e075d8e7 (diff)
parentc2830fa8f84f61462098bf36add824f89436dfa9 (diff)
downloadPeerTube-fada8d75550dc7365f7e18ee1569b9406251d660.tar.gz
PeerTube-fada8d75550dc7365f7e18ee1569b9406251d660.tar.zst
PeerTube-fada8d75550dc7365f7e18ee1569b9406251d660.zip
Merge branch 'feature/design' into develop
-rw-r--r--CREDITS.md9
-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
-rw-r--r--config/default.yaml1
-rw-r--r--config/production.yaml.example1
-rw-r--r--config/test-1.yaml1
-rw-r--r--config/test-2.yaml1
-rw-r--r--config/test-3.yaml1
-rw-r--r--config/test-4.yaml1
-rw-r--r--config/test-5.yaml1
-rw-r--r--config/test-6.yaml1
-rw-r--r--server/controllers/api/videos/index.ts139
-rw-r--r--server/controllers/client.ts2
-rw-r--r--server/helpers/custom-validators/activitypub/videos.ts6
-rw-r--r--server/helpers/custom-validators/videos.ts8
-rw-r--r--server/initializers/constants.ts13
-rw-r--r--server/initializers/database.ts2
-rw-r--r--server/initializers/migrations/0115-account-avatar.ts31
-rw-r--r--server/initializers/migrations/0120-video-null.ts47
-rw-r--r--server/lib/activitypub/process/misc.ts21
-rw-r--r--server/middlewares/index.ts1
-rw-r--r--server/middlewares/search.ts14
-rw-r--r--server/middlewares/validators/videos.ts11
-rw-r--r--server/models/account/account-interface.ts3
-rw-r--r--server/models/account/account.ts26
-rw-r--r--server/models/account/user.ts5
-rw-r--r--server/models/avatar/avatar-interface.ts16
-rw-r--r--server/models/avatar/avatar.ts24
-rw-r--r--server/models/avatar/index.ts1
-rw-r--r--server/models/index.ts1
-rw-r--r--server/models/video/video-interface.ts1
-rw-r--r--server/models/video/video.ts92
-rw-r--r--server/tests/api/check-params/videos.ts30
-rw-r--r--server/tests/api/follows.ts2
-rw-r--r--server/tests/api/multiple-servers.ts57
-rw-r--r--server/tests/api/services.ts4
-rw-r--r--server/tests/api/single-server.ts185
-rw-r--r--server/tests/api/users.ts8
-rw-r--r--server/tests/utils/servers.ts2
-rw-r--r--server/tests/utils/videos.ts22
-rw-r--r--shared/models/accounts/account.model.ts8
-rw-r--r--shared/models/avatars/avatar.model.ts5
-rw-r--r--shared/models/users/user.model.ts8
-rw-r--r--shared/models/videos/video-create.model.ts10
-rw-r--r--shared/models/videos/video.model.ts4
244 files changed, 4713 insertions, 3244 deletions
diff --git a/CREDITS.md b/CREDITS.md
index a7b2b5568..65017bbc1 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -8,14 +8,9 @@
8 8
9# Design 9# Design
10 10
11Inspirations from: 11By [Olivier Massain](https://twitter.com/omassain)
12 12
13 * [Aurélien Salomon](https://dribbble.com/shots/1338727-Youtube-Redesign) 13Icons from [Robbie Pearce](https://robbiepearce.com/softies/)
14 * [Wojciech Zieliński](https://dribbble.com/shots/3000315-youtube-concept)
15
16Video.js theme:
17
18 * [zanechua](https://github.com/zanechua/videojs-sublime-inspired-skin)
19 14
20# Fonts 15# Fonts
21 16
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"
diff --git a/config/default.yaml b/config/default.yaml
index b53fa0d5b..2c1043067 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -16,6 +16,7 @@ database:
16 16
17# From the project root directory 17# From the project root directory
18storage: 18storage:
19 avatars: 'avatars/'
19 certs: 'certs/' 20 certs: 'certs/'
20 videos: 'videos/' 21 videos: 'videos/'
21 logs: 'logs/' 22 logs: 'logs/'
diff --git a/config/production.yaml.example b/config/production.yaml.example
index 1af20a9e4..404d35c16 100644
--- a/config/production.yaml.example
+++ b/config/production.yaml.example
@@ -17,6 +17,7 @@ database:
17 17
18# From the project root directory 18# From the project root directory
19storage: 19storage:
20 avatars: 'avatars/'
20 certs: 'certs/' 21 certs: 'certs/'
21 videos: 'videos/' 22 videos: 'videos/'
22 logs: 'logs/' 23 logs: 'logs/'
diff --git a/config/test-1.yaml b/config/test-1.yaml
index d9b4d2b1a..49fbebf04 100644
--- a/config/test-1.yaml
+++ b/config/test-1.yaml
@@ -10,6 +10,7 @@ database:
10 10
11# From the project root directory 11# From the project root directory
12storage: 12storage:
13 avatars: 'test1/avatars/'
13 certs: 'test1/certs/' 14 certs: 'test1/certs/'
14 videos: 'test1/videos/' 15 videos: 'test1/videos/'
15 logs: 'test1/logs/' 16 logs: 'test1/logs/'
diff --git a/config/test-2.yaml b/config/test-2.yaml
index 236dcb10d..ff0df5962 100644
--- a/config/test-2.yaml
+++ b/config/test-2.yaml
@@ -10,6 +10,7 @@ database:
10 10
11# From the project root directory 11# From the project root directory
12storage: 12storage:
13 avatars: 'test2/avatars/'
13 certs: 'test2/certs/' 14 certs: 'test2/certs/'
14 videos: 'test2/videos/' 15 videos: 'test2/videos/'
15 logs: 'test2/logs/' 16 logs: 'test2/logs/'
diff --git a/config/test-3.yaml b/config/test-3.yaml
index 291b43edc..4fbb00050 100644
--- a/config/test-3.yaml
+++ b/config/test-3.yaml
@@ -10,6 +10,7 @@ database:
10 10
11# From the project root directory 11# From the project root directory
12storage: 12storage:
13 avatars: 'test3/avatars/'
13 certs: 'test3/certs/' 14 certs: 'test3/certs/'
14 videos: 'test3/videos/' 15 videos: 'test3/videos/'
15 logs: 'test3/logs/' 16 logs: 'test3/logs/'
diff --git a/config/test-4.yaml b/config/test-4.yaml
index 6f80939fc..e4f0f2691 100644
--- a/config/test-4.yaml
+++ b/config/test-4.yaml
@@ -10,6 +10,7 @@ database:
10 10
11# From the project root directory 11# From the project root directory
12storage: 12storage:
13 avatars: 'test4/avatars/'
13 certs: 'test4/certs/' 14 certs: 'test4/certs/'
14 videos: 'test4/videos/' 15 videos: 'test4/videos/'
15 logs: 'test4/logs/' 16 logs: 'test4/logs/'
diff --git a/config/test-5.yaml b/config/test-5.yaml
index 0b5eab72e..610f523c8 100644
--- a/config/test-5.yaml
+++ b/config/test-5.yaml
@@ -10,6 +10,7 @@ database:
10 10
11# From the project root directory 11# From the project root directory
12storage: 12storage:
13 avatars: 'test5/avatars/'
13 certs: 'test5/certs/' 14 certs: 'test5/certs/'
14 videos: 'test5/videos/' 15 videos: 'test5/videos/'
15 logs: 'test5/logs/' 16 logs: 'test5/logs/'
diff --git a/config/test-6.yaml b/config/test-6.yaml
index 5d33e45b9..088b55c17 100644
--- a/config/test-6.yaml
+++ b/config/test-6.yaml
@@ -10,6 +10,7 @@ database:
10 10
11# From the project root directory 11# From the project root directory
12storage: 12storage:
13 avatars: 'test6/avatars/'
13 certs: 'test6/certs/' 14 certs: 'test6/certs/'
14 videos: 'test6/videos/' 15 videos: 'test6/videos/'
15 logs: 'test6/logs/' 16 logs: 'test6/logs/'
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index e2798830e..63de662a7 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -15,6 +15,7 @@ import { getServerAccount } from '../../../helpers/utils'
15import { CONFIG, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_MIMETYPE_EXT, VIDEO_PRIVACIES } from '../../../initializers' 15import { CONFIG, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_MIMETYPE_EXT, VIDEO_PRIVACIES } from '../../../initializers'
16import { database as db } from '../../../initializers/database' 16import { database as db } from '../../../initializers/database'
17import { sendAddVideo } from '../../../lib/activitypub/send/send-add' 17import { sendAddVideo } from '../../../lib/activitypub/send/send-add'
18import { sendCreateViewToOrigin } from '../../../lib/activitypub/send/send-create'
18import { sendUpdateVideo } from '../../../lib/activitypub/send/send-update' 19import { sendUpdateVideo } from '../../../lib/activitypub/send/send-update'
19import { shareVideoByServer } from '../../../lib/activitypub/share' 20import { shareVideoByServer } from '../../../lib/activitypub/share'
20import { getVideoActivityPubUrl } from '../../../lib/activitypub/url' 21import { getVideoActivityPubUrl } from '../../../lib/activitypub/url'
@@ -26,7 +27,6 @@ import {
26 authenticate, 27 authenticate,
27 paginationValidator, 28 paginationValidator,
28 setPagination, 29 setPagination,
29 setVideosSearch,
30 setVideosSort, 30 setVideosSort,
31 videosAddValidator, 31 videosAddValidator,
32 videosGetValidator, 32 videosGetValidator,
@@ -40,7 +40,6 @@ import { abuseVideoRouter } from './abuse'
40import { blacklistRouter } from './blacklist' 40import { blacklistRouter } from './blacklist'
41import { videoChannelRouter } from './channel' 41import { videoChannelRouter } from './channel'
42import { rateVideoRouter } from './rate' 42import { rateVideoRouter } from './rate'
43import { sendCreateViewToOrigin } from '../../../lib/activitypub/send/send-create'
44 43
45const videosRouter = express.Router() 44const videosRouter = express.Router()
46 45
@@ -84,6 +83,14 @@ videosRouter.get('/',
84 setPagination, 83 setPagination,
85 asyncMiddleware(listVideos) 84 asyncMiddleware(listVideos)
86) 85)
86videosRouter.get('/search',
87 videosSearchValidator,
88 paginationValidator,
89 videosSortValidator,
90 setVideosSort,
91 setPagination,
92 asyncMiddleware(searchVideos)
93)
87videosRouter.put('/:id', 94videosRouter.put('/:id',
88 authenticate, 95 authenticate,
89 asyncMiddleware(videosUpdateValidator), 96 asyncMiddleware(videosUpdateValidator),
@@ -115,16 +122,6 @@ videosRouter.delete('/:id',
115 asyncMiddleware(removeVideoRetryWrapper) 122 asyncMiddleware(removeVideoRetryWrapper)
116) 123)
117 124
118videosRouter.get('/search/:value',
119 videosSearchValidator,
120 paginationValidator,
121 videosSortValidator,
122 setVideosSort,
123 setPagination,
124 setVideosSearch,
125 asyncMiddleware(searchVideos)
126)
127
128// --------------------------------------------------------------------------- 125// ---------------------------------------------------------------------------
129 126
130export { 127export {
@@ -157,59 +154,64 @@ async function addVideoRetryWrapper (req: express.Request, res: express.Response
157 errorMessage: 'Cannot insert the video with many retries.' 154 errorMessage: 'Cannot insert the video with many retries.'
158 } 155 }
159 156
160 await retryTransactionWrapper(addVideo, options) 157 const video = await retryTransactionWrapper(addVideo, options)
161 158
162 // TODO : include Location of the new video -> 201 159 res.json({
163 res.type('json').status(204).end() 160 video: {
161 id: video.id,
162 uuid: video.uuid
163 }
164 }).end()
164} 165}
165 166
166async function addVideo (req: express.Request, res: express.Response, videoPhysicalFile: Express.Multer.File) { 167async function addVideo (req: express.Request, res: express.Response, videoPhysicalFile: Express.Multer.File) {
167 const videoInfo: VideoCreate = req.body 168 const videoInfo: VideoCreate = req.body
168 let videoUUID = ''
169 169
170 await db.sequelize.transaction(async t => { 170 // Prepare data so we don't block the transaction
171 const sequelizeOptions = { transaction: t } 171 const videoData = {
172 name: videoInfo.name,
173 remote: false,
174 extname: extname(videoPhysicalFile.filename),
175 category: videoInfo.category,
176 licence: videoInfo.licence,
177 language: videoInfo.language,
178 nsfw: videoInfo.nsfw,
179 description: videoInfo.description,
180 privacy: videoInfo.privacy,
181 duration: videoPhysicalFile['duration'], // duration was added by a previous middleware
182 channelId: res.locals.videoChannel.id
183 }
184 const video = db.Video.build(videoData)
185 video.url = getVideoActivityPubUrl(video)
172 186
173 const videoData = { 187 const videoFilePath = join(CONFIG.STORAGE.VIDEOS_DIR, videoPhysicalFile.filename)
174 name: videoInfo.name, 188 const videoFileHeight = await getVideoFileHeight(videoFilePath)
175 remote: false,
176 extname: extname(videoPhysicalFile.filename),
177 category: videoInfo.category,
178 licence: videoInfo.licence,
179 language: videoInfo.language,
180 nsfw: videoInfo.nsfw,
181 description: videoInfo.description,
182 privacy: videoInfo.privacy,
183 duration: videoPhysicalFile['duration'], // duration was added by a previous middleware
184 channelId: res.locals.videoChannel.id
185 }
186 const video = db.Video.build(videoData)
187 video.url = getVideoActivityPubUrl(video)
188 189
189 const videoFilePath = join(CONFIG.STORAGE.VIDEOS_DIR, videoPhysicalFile.filename) 190 const videoFileData = {
190 const videoFileHeight = await getVideoFileHeight(videoFilePath) 191 extname: extname(videoPhysicalFile.filename),
192 resolution: videoFileHeight,
193 size: videoPhysicalFile.size
194 }
195 const videoFile = db.VideoFile.build(videoFileData)
196 const videoDir = CONFIG.STORAGE.VIDEOS_DIR
197 const source = join(videoDir, videoPhysicalFile.filename)
198 const destination = join(videoDir, video.getVideoFilename(videoFile))
191 199
192 const videoFileData = { 200 await renamePromise(source, destination)
193 extname: extname(videoPhysicalFile.filename), 201 // This is important in case if there is another attempt in the retry process
194 resolution: videoFileHeight, 202 videoPhysicalFile.filename = video.getVideoFilename(videoFile)
195 size: videoPhysicalFile.size
196 }
197 const videoFile = db.VideoFile.build(videoFileData)
198 const videoDir = CONFIG.STORAGE.VIDEOS_DIR
199 const source = join(videoDir, videoPhysicalFile.filename)
200 const destination = join(videoDir, video.getVideoFilename(videoFile))
201 203
202 await renamePromise(source, destination) 204 const tasks = []
203 // This is important in case if there is another attempt in the retry process
204 videoPhysicalFile.filename = video.getVideoFilename(videoFile)
205 205
206 const tasks = [] 206 tasks.push(
207 video.createTorrentAndSetInfoHash(videoFile),
208 video.createThumbnail(videoFile),
209 video.createPreview(videoFile)
210 )
211 await Promise.all(tasks)
207 212
208 tasks.push( 213 return db.sequelize.transaction(async t => {
209 video.createTorrentAndSetInfoHash(videoFile), 214 const sequelizeOptions = { transaction: t }
210 video.createThumbnail(videoFile),
211 video.createPreview(videoFile)
212 )
213 215
214 if (CONFIG.TRANSCODING.ENABLED === true) { 216 if (CONFIG.TRANSCODING.ENABLED === true) {
215 // Put uuid because we don't have id auto incremented for now 217 // Put uuid because we don't have id auto incremented for now
@@ -217,21 +219,17 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
217 videoUUID: video.uuid 219 videoUUID: video.uuid
218 } 220 }
219 221
220 tasks.push( 222 await transcodingJobScheduler.createJob(t, 'videoFileOptimizer', dataInput)
221 transcodingJobScheduler.createJob(t, 'videoFileOptimizer', dataInput)
222 )
223 } 223 }
224 await Promise.all(tasks)
225 224
226 const videoCreated = await video.save(sequelizeOptions) 225 const videoCreated = await video.save(sequelizeOptions)
227 // Do not forget to add video channel information to the created video 226 // Do not forget to add video channel information to the created video
228 videoCreated.VideoChannel = res.locals.videoChannel 227 videoCreated.VideoChannel = res.locals.videoChannel
229 videoUUID = videoCreated.uuid
230 228
231 videoFile.videoId = video.id 229 videoFile.videoId = video.id
232
233 await videoFile.save(sequelizeOptions) 230 await videoFile.save(sequelizeOptions)
234 video.VideoFiles = [videoFile] 231
232 video.VideoFiles = [ videoFile ]
235 233
236 if (videoInfo.tags) { 234 if (videoInfo.tags) {
237 const tagInstances = await db.Tag.findOrCreateTags(videoInfo.tags, t) 235 const tagInstances = await db.Tag.findOrCreateTags(videoInfo.tags, t)
@@ -241,15 +239,17 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
241 } 239 }
242 240
243 // Let transcoding job send the video to friends because the video file extension might change 241 // Let transcoding job send the video to friends because the video file extension might change
244 if (CONFIG.TRANSCODING.ENABLED === true) return undefined 242 if (CONFIG.TRANSCODING.ENABLED === true) return videoCreated
245 // Don't send video to remote servers, it is private 243 // Don't send video to remote servers, it is private
246 if (video.privacy === VideoPrivacy.PRIVATE) return undefined 244 if (video.privacy === VideoPrivacy.PRIVATE) return videoCreated
247 245
248 await sendAddVideo(video, t) 246 await sendAddVideo(video, t)
249 await shareVideoByServer(video, t) 247 await shareVideoByServer(video, t)
250 })
251 248
252 logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoUUID) 249 logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid)
250
251 return videoCreated
252 })
253} 253}
254 254
255async function updateVideoRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { 255async function updateVideoRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
@@ -280,7 +280,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
280 if (videoInfoToUpdate.licence !== undefined) videoInstance.set('licence', videoInfoToUpdate.licence) 280 if (videoInfoToUpdate.licence !== undefined) videoInstance.set('licence', videoInfoToUpdate.licence)
281 if (videoInfoToUpdate.language !== undefined) videoInstance.set('language', videoInfoToUpdate.language) 281 if (videoInfoToUpdate.language !== undefined) videoInstance.set('language', videoInfoToUpdate.language)
282 if (videoInfoToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfoToUpdate.nsfw) 282 if (videoInfoToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfoToUpdate.nsfw)
283 if (videoInfoToUpdate.privacy !== undefined) videoInstance.set('privacy', videoInfoToUpdate.privacy) 283 if (videoInfoToUpdate.privacy !== undefined) videoInstance.set('privacy', parseInt(videoInfoToUpdate.privacy.toString(), 10))
284 if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description) 284 if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description)
285 285
286 const videoInstanceUpdated = await videoInstance.save(sequelizeOptions) 286 const videoInstanceUpdated = await videoInstance.save(sequelizeOptions)
@@ -298,9 +298,9 @@ async function updateVideo (req: express.Request, res: express.Response) {
298 } 298 }
299 299
300 // Video is not private anymore, send a create action to remote servers 300 // Video is not private anymore, send a create action to remote servers
301 if (wasPrivateVideo === true && videoInstance.privacy !== VideoPrivacy.PRIVATE) { 301 if (wasPrivateVideo === true && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE) {
302 await sendAddVideo(videoInstance, t) 302 await sendAddVideo(videoInstanceUpdated, t)
303 await shareVideoByServer(videoInstance, t) 303 await shareVideoByServer(videoInstanceUpdated, t)
304 } 304 }
305 }) 305 })
306 306
@@ -378,8 +378,7 @@ async function removeVideo (req: express.Request, res: express.Response) {
378 378
379async function searchVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 379async function searchVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
380 const resultList = await db.Video.searchAndPopulateAccountAndServerAndTags( 380 const resultList = await db.Video.searchAndPopulateAccountAndServerAndTags(
381 req.params.value, 381 req.query.search,
382 req.query.field,
383 req.query.start, 382 req.query.start,
384 req.query.count, 383 req.query.count,
385 req.query.sort 384 req.query.sort
diff --git a/server/controllers/client.ts b/server/controllers/client.ts
index 64e5829ca..f474c4282 100644
--- a/server/controllers/client.ts
+++ b/server/controllers/client.ts
@@ -18,6 +18,7 @@ import { VideoInstance } from '../models'
18const clientsRouter = express.Router() 18const clientsRouter = express.Router()
19 19
20const distPath = join(root(), 'client', 'dist') 20const distPath = join(root(), 'client', 'dist')
21const assetsImagesPath = join(root(), 'client', 'dist', 'assets', 'images')
21const embedPath = join(distPath, 'standalone', 'videos', 'embed.html') 22const embedPath = join(distPath, 'standalone', 'videos', 'embed.html')
22const indexPath = join(distPath, 'index.html') 23const indexPath = join(distPath, 'index.html')
23 24
@@ -33,6 +34,7 @@ clientsRouter.use('/videos/embed', (req: express.Request, res: express.Response,
33 34
34// Static HTML/CSS/JS client files 35// Static HTML/CSS/JS client files
35clientsRouter.use('/client', express.static(distPath, { maxAge: STATIC_MAX_AGE })) 36clientsRouter.use('/client', express.static(distPath, { maxAge: STATIC_MAX_AGE }))
37clientsRouter.use('/client/assets/images', express.static(assetsImagesPath, { maxAge: STATIC_MAX_AGE }))
36 38
37// 404 for static files not found 39// 404 for static files not found
38clientsRouter.use('/client/*', (req: express.Request, res: express.Response, next: express.NextFunction) => { 40clientsRouter.use('/client/*', (req: express.Request, res: express.Response, next: express.NextFunction) => {
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts
index 12c672fd2..2ed2988f5 100644
--- a/server/helpers/custom-validators/activitypub/videos.ts
+++ b/server/helpers/custom-validators/activitypub/videos.ts
@@ -49,14 +49,14 @@ function isVideoTorrentObjectValid (video: any) {
49 isActivityPubVideoDurationValid(video.duration) && 49 isActivityPubVideoDurationValid(video.duration) &&
50 isUUIDValid(video.uuid) && 50 isUUIDValid(video.uuid) &&
51 setValidRemoteTags(video) && 51 setValidRemoteTags(video) &&
52 isRemoteIdentifierValid(video.category) && 52 (!video.category || isRemoteIdentifierValid(video.category)) &&
53 isRemoteIdentifierValid(video.licence) && 53 (!video.licence || isRemoteIdentifierValid(video.licence)) &&
54 (!video.language || isRemoteIdentifierValid(video.language)) && 54 (!video.language || isRemoteIdentifierValid(video.language)) &&
55 isVideoViewsValid(video.views) && 55 isVideoViewsValid(video.views) &&
56 isVideoNSFWValid(video.nsfw) && 56 isVideoNSFWValid(video.nsfw) &&
57 isDateValid(video.published) && 57 isDateValid(video.published) &&
58 isDateValid(video.updated) && 58 isDateValid(video.updated) &&
59 isRemoteVideoContentValid(video.mediaType, video.content) && 59 (!video.content || isRemoteVideoContentValid(video.mediaType, video.content)) &&
60 isRemoteVideoIconValid(video.icon) && 60 isRemoteVideoIconValid(video.icon) &&
61 setValidRemoteVideoUrls(video) && 61 setValidRemoteVideoUrls(video) &&
62 video.url.length !== 0 62 video.url.length !== 0
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts
index f13178c54..37fa8b08a 100644
--- a/server/helpers/custom-validators/videos.ts
+++ b/server/helpers/custom-validators/videos.ts
@@ -14,11 +14,11 @@ const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
14const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES 14const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES
15 15
16function isVideoCategoryValid (value: number) { 16function isVideoCategoryValid (value: number) {
17 return VIDEO_CATEGORIES[value] !== undefined 17 return value === null || VIDEO_CATEGORIES[value] !== undefined
18} 18}
19 19
20function isVideoLicenceValid (value: number) { 20function isVideoLicenceValid (value: number) {
21 return VIDEO_LICENCES[value] !== undefined 21 return value === null || VIDEO_LICENCES[value] !== undefined
22} 22}
23 23
24function isVideoLanguageValid (value: number) { 24function isVideoLanguageValid (value: number) {
@@ -38,7 +38,7 @@ function isVideoTruncatedDescriptionValid (value: string) {
38} 38}
39 39
40function isVideoDescriptionValid (value: string) { 40function isVideoDescriptionValid (value: string) {
41 return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.DESCRIPTION) 41 return value === null || (exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.DESCRIPTION))
42} 42}
43 43
44function isVideoNameValid (value: string) { 44function isVideoNameValid (value: string) {
@@ -84,7 +84,7 @@ function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } |
84} 84}
85 85
86function isVideoPrivacyValid (value: string) { 86function isVideoPrivacyValid (value: string) {
87 return VIDEO_PRIVACIES[value] !== undefined 87 return validator.isInt(value + '') && VIDEO_PRIVACIES[value] !== undefined
88} 88}
89 89
90function isVideoFileInfoHashValid (value: string) { 90function isVideoFileInfoHashValid (value: string) {
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index e3d779456..7be7a5f95 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -14,7 +14,7 @@ import { FollowState } from '../../shared/models/accounts/follow.model'
14 14
15// --------------------------------------------------------------------------- 15// ---------------------------------------------------------------------------
16 16
17const LAST_MIGRATION_VERSION = 110 17const LAST_MIGRATION_VERSION = 120
18 18
19// --------------------------------------------------------------------------- 19// ---------------------------------------------------------------------------
20 20
@@ -25,11 +25,6 @@ const API_VERSION = 'v1'
25const PAGINATION_COUNT_DEFAULT = 15 25const PAGINATION_COUNT_DEFAULT = 15
26 26
27// Sortable columns per schema 27// Sortable columns per schema
28const SEARCHABLE_COLUMNS = {
29 VIDEOS: [ 'name', 'magnetUri', 'host', 'account', 'tags' ]
30}
31
32// Sortable columns per schema
33const SORTABLE_COLUMNS = { 28const SORTABLE_COLUMNS = {
34 USERS: [ 'id', 'username', 'createdAt' ], 29 USERS: [ 'id', 'username', 'createdAt' ],
35 JOBS: [ 'id', 'createdAt' ], 30 JOBS: [ 'id', 'createdAt' ],
@@ -60,6 +55,7 @@ const CONFIG = {
60 PASSWORD: config.get<string>('database.password') 55 PASSWORD: config.get<string>('database.password')
61 }, 56 },
62 STORAGE: { 57 STORAGE: {
58 AVATARS_DIR: join(root(), config.get<string>('storage.avatars')),
63 LOG_DIR: join(root(), config.get<string>('storage.logs')), 59 LOG_DIR: join(root(), config.get<string>('storage.logs')),
64 VIDEOS_DIR: join(root(), config.get<string>('storage.videos')), 60 VIDEOS_DIR: join(root(), config.get<string>('storage.videos')),
65 THUMBNAILS_DIR: join(root(), config.get<string>('storage.thumbnails')), 61 THUMBNAILS_DIR: join(root(), config.get<string>('storage.thumbnails')),
@@ -105,6 +101,9 @@ const CONFIG = {
105CONFIG.WEBSERVER.URL = CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT 101CONFIG.WEBSERVER.URL = CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
106CONFIG.WEBSERVER.HOST = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT 102CONFIG.WEBSERVER.HOST = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
107 103
104const AVATARS_DIR = {
105 ACCOUNT: join(CONFIG.STORAGE.AVATARS_DIR, 'account')
106}
108// --------------------------------------------------------------------------- 107// ---------------------------------------------------------------------------
109 108
110const CONSTRAINTS_FIELDS = { 109const CONSTRAINTS_FIELDS = {
@@ -356,7 +355,7 @@ export {
356 PREVIEWS_SIZE, 355 PREVIEWS_SIZE,
357 REMOTE_SCHEME, 356 REMOTE_SCHEME,
358 FOLLOW_STATES, 357 FOLLOW_STATES,
359 SEARCHABLE_COLUMNS, 358 AVATARS_DIR,
360 SERVER_ACCOUNT_NAME, 359 SERVER_ACCOUNT_NAME,
361 PRIVATE_RSA_KEY_SIZE, 360 PRIVATE_RSA_KEY_SIZE,
362 SORTABLE_COLUMNS, 361 SORTABLE_COLUMNS,
diff --git a/server/initializers/database.ts b/server/initializers/database.ts
index 90dbba5b9..bb95992e1 100644
--- a/server/initializers/database.ts
+++ b/server/initializers/database.ts
@@ -2,6 +2,7 @@ import { join } from 'path'
2import { flattenDepth } from 'lodash' 2import { flattenDepth } from 'lodash'
3require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string 3require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
4import * as Sequelize from 'sequelize' 4import * as Sequelize from 'sequelize'
5import { AvatarModel } from '../models/avatar'
5 6
6import { CONFIG } from './constants' 7import { CONFIG } from './constants'
7// Do not use barrel, we need to load database first 8// Do not use barrel, we need to load database first
@@ -36,6 +37,7 @@ export type PeerTubeDatabase = {
36 init?: (silent: boolean) => Promise<void>, 37 init?: (silent: boolean) => Promise<void>,
37 38
38 Application?: ApplicationModel, 39 Application?: ApplicationModel,
40 Avatar?: AvatarModel,
39 Account?: AccountModel, 41 Account?: AccountModel,
40 Job?: JobModel, 42 Job?: JobModel,
41 OAuthClient?: OAuthClientModel, 43 OAuthClient?: OAuthClientModel,
diff --git a/server/initializers/migrations/0115-account-avatar.ts b/server/initializers/migrations/0115-account-avatar.ts
new file mode 100644
index 000000000..2b947ceda
--- /dev/null
+++ b/server/initializers/migrations/0115-account-avatar.ts
@@ -0,0 +1,31 @@
1import * as Sequelize from 'sequelize'
2import { PeerTubeDatabase } from '../database'
3
4async function up (utils: {
5 transaction: Sequelize.Transaction,
6 queryInterface: Sequelize.QueryInterface,
7 sequelize: Sequelize.Sequelize,
8 db: PeerTubeDatabase
9}): Promise<void> {
10 await utils.db.Avatar.sync()
11
12 const data = {
13 type: Sequelize.INTEGER,
14 allowNull: true,
15 references: {
16 model: 'Avatars',
17 key: 'id'
18 },
19 onDelete: 'CASCADE'
20 }
21 await utils.queryInterface.addColumn('Accounts', 'avatarId', data)
22}
23
24function down (options) {
25 throw new Error('Not implemented.')
26}
27
28export {
29 up,
30 down
31}
diff --git a/server/initializers/migrations/0120-video-null.ts b/server/initializers/migrations/0120-video-null.ts
new file mode 100644
index 000000000..9130d10ee
--- /dev/null
+++ b/server/initializers/migrations/0120-video-null.ts
@@ -0,0 +1,47 @@
1import * as Sequelize from 'sequelize'
2import { CONSTRAINTS_FIELDS } from '../constants'
3import { PeerTubeDatabase } from '../database'
4
5async function up (utils: {
6 transaction: Sequelize.Transaction,
7 queryInterface: Sequelize.QueryInterface,
8 sequelize: Sequelize.Sequelize,
9 db: PeerTubeDatabase
10}): Promise<void> {
11
12 {
13 const data = {
14 type: Sequelize.INTEGER,
15 allowNull: true,
16 defaultValue: null
17 }
18 await utils.queryInterface.changeColumn('Videos', 'licence', data)
19 }
20
21 {
22 const data = {
23 type: Sequelize.INTEGER,
24 allowNull: true,
25 defaultValue: null
26 }
27 await utils.queryInterface.changeColumn('Videos', 'category', data)
28 }
29
30 {
31 const data = {
32 type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max),
33 allowNull: true,
34 defaultValue: null
35 }
36 await utils.queryInterface.changeColumn('Videos', 'description', data)
37 }
38}
39
40function down (options) {
41 throw new Error('Not implemented.')
42}
43
44export {
45 up,
46 down
47}
diff --git a/server/lib/activitypub/process/misc.ts b/server/lib/activitypub/process/misc.ts
index f20e588ab..0baa22c26 100644
--- a/server/lib/activitypub/process/misc.ts
+++ b/server/lib/activitypub/process/misc.ts
@@ -41,15 +41,30 @@ async function videoActivityObjectToDBAttributes (
41 language = parseInt(videoObject.language.identifier, 10) 41 language = parseInt(videoObject.language.identifier, 10)
42 } 42 }
43 43
44 let category = null
45 if (videoObject.category) {
46 category = parseInt(videoObject.category.identifier, 10)
47 }
48
49 let licence = null
50 if (videoObject.licence) {
51 licence = parseInt(videoObject.licence.identifier, 10)
52 }
53
54 let description = null
55 if (videoObject.content) {
56 description = videoObject.content
57 }
58
44 const videoData: VideoAttributes = { 59 const videoData: VideoAttributes = {
45 name: videoObject.name, 60 name: videoObject.name,
46 uuid: videoObject.uuid, 61 uuid: videoObject.uuid,
47 url: videoObject.id, 62 url: videoObject.id,
48 category: parseInt(videoObject.category.identifier, 10), 63 category,
49 licence: parseInt(videoObject.licence.identifier, 10), 64 licence,
50 language, 65 language,
66 description,
51 nsfw: videoObject.nsfw, 67 nsfw: videoObject.nsfw,
52 description: videoObject.content,
53 channelId: videoChannel.id, 68 channelId: videoChannel.id,
54 duration: parseInt(duration, 10), 69 duration: parseInt(duration, 10),
55 createdAt: new Date(videoObject.published), 70 createdAt: new Date(videoObject.published),
diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts
index aafcad2d9..0cef26953 100644
--- a/server/middlewares/index.ts
+++ b/server/middlewares/index.ts
@@ -4,6 +4,5 @@ export * from './async'
4export * from './oauth' 4export * from './oauth'
5export * from './pagination' 5export * from './pagination'
6export * from './servers' 6export * from './servers'
7export * from './search'
8export * from './sort' 7export * from './sort'
9export * from './user-right' 8export * from './user-right'
diff --git a/server/middlewares/search.ts b/server/middlewares/search.ts
deleted file mode 100644
index 6fe83d25b..000000000
--- a/server/middlewares/search.ts
+++ /dev/null
@@ -1,14 +0,0 @@
1import 'express-validator'
2import * as express from 'express'
3
4function setVideosSearch (req: express.Request, res: express.Response, next: express.NextFunction) {
5 if (!req.query.field) req.query.field = 'name'
6
7 return next()
8}
9
10// ---------------------------------------------------------------------------
11
12export {
13 setVideosSearch
14}
diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos.ts
index f21680aa0..10625e41d 100644
--- a/server/middlewares/validators/videos.ts
+++ b/server/middlewares/validators/videos.ts
@@ -18,7 +18,7 @@ import {
18} from '../../helpers/custom-validators/videos' 18} from '../../helpers/custom-validators/videos'
19import { getDurationFromVideoFile } from '../../helpers/ffmpeg-utils' 19import { getDurationFromVideoFile } from '../../helpers/ffmpeg-utils'
20import { logger } from '../../helpers/logger' 20import { logger } from '../../helpers/logger'
21import { CONSTRAINTS_FIELDS, SEARCHABLE_COLUMNS } from '../../initializers' 21import { CONSTRAINTS_FIELDS } from '../../initializers'
22import { database as db } from '../../initializers/database' 22import { database as db } from '../../initializers/database'
23import { UserInstance } from '../../models/account/user-interface' 23import { UserInstance } from '../../models/account/user-interface'
24import { VideoInstance } from '../../models/video/video-interface' 24import { VideoInstance } from '../../models/video/video-interface'
@@ -31,11 +31,11 @@ const videosAddValidator = [
31 + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ') 31 + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
32 ), 32 ),
33 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'), 33 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
34 body('category').custom(isVideoCategoryValid).withMessage('Should have a valid category'), 34 body('category').optional().custom(isVideoCategoryValid).withMessage('Should have a valid category'),
35 body('licence').custom(isVideoLicenceValid).withMessage('Should have a valid licence'), 35 body('licence').optional().custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
36 body('language').optional().custom(isVideoLanguageValid).withMessage('Should have a valid language'), 36 body('language').optional().custom(isVideoLanguageValid).withMessage('Should have a valid language'),
37 body('nsfw').custom(isVideoNSFWValid).withMessage('Should have a valid NSFW attribute'), 37 body('nsfw').custom(isVideoNSFWValid).withMessage('Should have a valid NSFW attribute'),
38 body('description').custom(isVideoDescriptionValid).withMessage('Should have a valid description'), 38 body('description').optional().custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
39 body('channelId').custom(isIdValid).withMessage('Should have correct video channel id'), 39 body('channelId').custom(isIdValid).withMessage('Should have correct video channel id'),
40 body('privacy').custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'), 40 body('privacy').custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
41 body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'), 41 body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'),
@@ -172,8 +172,7 @@ const videosRemoveValidator = [
172] 172]
173 173
174const videosSearchValidator = [ 174const videosSearchValidator = [
175 param('value').not().isEmpty().withMessage('Should have a valid search'), 175 query('search').not().isEmpty().withMessage('Should have a valid search'),
176 query('field').optional().isIn(SEARCHABLE_COLUMNS.VIDEOS).withMessage('Should have correct searchable column'),
177 176
178 (req: express.Request, res: express.Response, next: express.NextFunction) => { 177 (req: express.Request, res: express.Response, next: express.NextFunction) => {
179 logger.debug('Checking videosSearch parameters', { parameters: req.params }) 178 logger.debug('Checking videosSearch parameters', { parameters: req.params })
diff --git a/server/models/account/account-interface.ts b/server/models/account/account-interface.ts
index b369766dc..46fe068e3 100644
--- a/server/models/account/account-interface.ts
+++ b/server/models/account/account-interface.ts
@@ -1,6 +1,7 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import * as Sequelize from 'sequelize' 2import * as Sequelize from 'sequelize'
3import { Account as FormattedAccount, ActivityPubActor } from '../../../shared' 3import { Account as FormattedAccount, ActivityPubActor } from '../../../shared'
4import { AvatarInstance } from '../avatar'
4import { ServerInstance } from '../server/server-interface' 5import { ServerInstance } from '../server/server-interface'
5import { VideoChannelInstance } from '../video/video-channel-interface' 6import { VideoChannelInstance } from '../video/video-channel-interface'
6 7
@@ -51,6 +52,7 @@ export interface AccountAttributes {
51 serverId?: number 52 serverId?: number
52 userId?: number 53 userId?: number
53 applicationId?: number 54 applicationId?: number
55 avatarId?: number
54} 56}
55 57
56export interface AccountInstance extends AccountClass, AccountAttributes, Sequelize.Instance<AccountAttributes> { 58export interface AccountInstance extends AccountClass, AccountAttributes, Sequelize.Instance<AccountAttributes> {
@@ -68,6 +70,7 @@ export interface AccountInstance extends AccountClass, AccountAttributes, Sequel
68 70
69 Server: ServerInstance 71 Server: ServerInstance
70 VideoChannels: VideoChannelInstance[] 72 VideoChannels: VideoChannelInstance[]
73 Avatar: AvatarInstance
71} 74}
72 75
73export interface AccountModel extends AccountClass, Sequelize.Model<AccountInstance, AccountAttributes> {} 76export interface AccountModel extends AccountClass, Sequelize.Model<AccountInstance, AccountAttributes> {}
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 61a88524c..8b0819f39 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -1,4 +1,6 @@
1import { join } from 'path'
1import * as Sequelize from 'sequelize' 2import * as Sequelize from 'sequelize'
3import { Avatar } from '../../../shared/models/avatars/avatar.model'
2import { 4import {
3 activityPubContextify, 5 activityPubContextify,
4 isAccountFollowersCountValid, 6 isAccountFollowersCountValid,
@@ -8,6 +10,7 @@ import {
8 isUserUsernameValid 10 isUserUsernameValid
9} from '../../helpers' 11} from '../../helpers'
10import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 12import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
13import { AVATARS_DIR } from '../../initializers'
11import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers/constants' 14import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers/constants'
12import { sendDeleteAccount } from '../../lib/activitypub/send/send-delete' 15import { sendDeleteAccount } from '../../lib/activitypub/send/send-delete'
13import { addMethodsToModel } from '../utils' 16import { addMethodsToModel } from '../utils'
@@ -252,6 +255,14 @@ function associate (models) {
252 as: 'followers', 255 as: 'followers',
253 onDelete: 'cascade' 256 onDelete: 'cascade'
254 }) 257 })
258
259 Account.hasOne(models.Avatar, {
260 foreignKey: {
261 name: 'avatarId',
262 allowNull: true
263 },
264 onDelete: 'cascade'
265 })
255} 266}
256 267
257function afterDestroy (account: AccountInstance) { 268function afterDestroy (account: AccountInstance) {
@@ -265,6 +276,15 @@ function afterDestroy (account: AccountInstance) {
265toFormattedJSON = function (this: AccountInstance) { 276toFormattedJSON = function (this: AccountInstance) {
266 let host = CONFIG.WEBSERVER.HOST 277 let host = CONFIG.WEBSERVER.HOST
267 let score: number 278 let score: number
279 let avatar: Avatar = null
280
281 if (this.Avatar) {
282 avatar = {
283 path: join(AVATARS_DIR.ACCOUNT, this.Avatar.filename),
284 createdAt: this.Avatar.createdAt,
285 updatedAt: this.Avatar.updatedAt
286 }
287 }
268 288
269 if (this.Server) { 289 if (this.Server) {
270 host = this.Server.host 290 host = this.Server.host
@@ -273,11 +293,15 @@ toFormattedJSON = function (this: AccountInstance) {
273 293
274 const json = { 294 const json = {
275 id: this.id, 295 id: this.id,
296 uuid: this.uuid,
276 host, 297 host,
277 score, 298 score,
278 name: this.name, 299 name: this.name,
300 followingCount: this.followingCount,
301 followersCount: this.followersCount,
279 createdAt: this.createdAt, 302 createdAt: this.createdAt,
280 updatedAt: this.updatedAt 303 updatedAt: this.updatedAt,
304 avatar
281 } 305 }
282 306
283 return json 307 return json
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index 8f7c9b013..3705947c0 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -157,10 +157,7 @@ toFormattedJSON = function (this: UserInstance) {
157 roleLabel: USER_ROLE_LABELS[this.role], 157 roleLabel: USER_ROLE_LABELS[this.role],
158 videoQuota: this.videoQuota, 158 videoQuota: this.videoQuota,
159 createdAt: this.createdAt, 159 createdAt: this.createdAt,
160 account: { 160 account: this.Account.toFormattedJSON()
161 id: this.Account.id,
162 uuid: this.Account.uuid
163 }
164 } 161 }
165 162
166 if (Array.isArray(this.Account.VideoChannels) === true) { 163 if (Array.isArray(this.Account.VideoChannels) === true) {
diff --git a/server/models/avatar/avatar-interface.ts b/server/models/avatar/avatar-interface.ts
new file mode 100644
index 000000000..4af2b87b7
--- /dev/null
+++ b/server/models/avatar/avatar-interface.ts
@@ -0,0 +1,16 @@
1import * as Sequelize from 'sequelize'
2
3export namespace AvatarMethods {}
4
5export interface AvatarClass {}
6
7export interface AvatarAttributes {
8 filename: string
9}
10
11export interface AvatarInstance extends AvatarClass, AvatarAttributes, Sequelize.Instance<AvatarAttributes> {
12 createdAt: Date
13 updatedAt: Date
14}
15
16export interface AvatarModel extends AvatarClass, Sequelize.Model<AvatarInstance, AvatarAttributes> {}
diff --git a/server/models/avatar/avatar.ts b/server/models/avatar/avatar.ts
new file mode 100644
index 000000000..96308fd5f
--- /dev/null
+++ b/server/models/avatar/avatar.ts
@@ -0,0 +1,24 @@
1import * as Sequelize from 'sequelize'
2import { addMethodsToModel } from '../utils'
3import { AvatarAttributes, AvatarInstance } from './avatar-interface'
4
5let Avatar: Sequelize.Model<AvatarInstance, AvatarAttributes>
6
7export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
8 Avatar = sequelize.define<AvatarInstance, AvatarAttributes>('Avatar',
9 {
10 filename: {
11 type: DataTypes.STRING,
12 allowNull: false
13 }
14 },
15 {}
16 )
17
18 const classMethods = []
19 addMethodsToModel(Avatar, classMethods)
20
21 return Avatar
22}
23
24// ------------------------------ Statics ------------------------------
diff --git a/server/models/avatar/index.ts b/server/models/avatar/index.ts
new file mode 100644
index 000000000..877aed1ce
--- /dev/null
+++ b/server/models/avatar/index.ts
@@ -0,0 +1 @@
export * from './avatar-interface'
diff --git a/server/models/index.ts b/server/models/index.ts
index 65faa5294..fedd97dd1 100644
--- a/server/models/index.ts
+++ b/server/models/index.ts
@@ -1,4 +1,5 @@
1export * from './application' 1export * from './application'
2export * from './avatar'
2export * from './job' 3export * from './job'
3export * from './oauth' 4export * from './oauth'
4export * from './server' 5export * from './server'
diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts
index be140de86..2a63350af 100644
--- a/server/models/video/video-interface.ts
+++ b/server/models/video/video-interface.ts
@@ -50,7 +50,6 @@ export namespace VideoMethods {
50 export type ListUserVideosForApi = (userId: number, start: number, count: number, sort: string) => Bluebird< ResultList<VideoInstance> > 50 export type ListUserVideosForApi = (userId: number, start: number, count: number, sort: string) => Bluebird< ResultList<VideoInstance> >
51 export type SearchAndPopulateAccountAndServerAndTags = ( 51 export type SearchAndPopulateAccountAndServerAndTags = (
52 value: string, 52 value: string,
53 field: string,
54 start: number, 53 start: number,
55 count: number, 54 count: number,
56 sort: string 55 sort: string
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index f3469c1de..d46fdeebe 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -104,7 +104,8 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
104 }, 104 },
105 category: { 105 category: {
106 type: DataTypes.INTEGER, 106 type: DataTypes.INTEGER,
107 allowNull: false, 107 allowNull: true,
108 defaultValue: null,
108 validate: { 109 validate: {
109 categoryValid: value => { 110 categoryValid: value => {
110 const res = isVideoCategoryValid(value) 111 const res = isVideoCategoryValid(value)
@@ -114,7 +115,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
114 }, 115 },
115 licence: { 116 licence: {
116 type: DataTypes.INTEGER, 117 type: DataTypes.INTEGER,
117 allowNull: false, 118 allowNull: true,
118 defaultValue: null, 119 defaultValue: null,
119 validate: { 120 validate: {
120 licenceValid: value => { 121 licenceValid: value => {
@@ -126,6 +127,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
126 language: { 127 language: {
127 type: DataTypes.INTEGER, 128 type: DataTypes.INTEGER,
128 allowNull: true, 129 allowNull: true,
130 defaultValue: null,
129 validate: { 131 validate: {
130 languageValid: value => { 132 languageValid: value => {
131 const res = isVideoLanguageValid(value) 133 const res = isVideoLanguageValid(value)
@@ -155,7 +157,8 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
155 }, 157 },
156 description: { 158 description: {
157 type: DataTypes.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max), 159 type: DataTypes.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max),
158 allowNull: false, 160 allowNull: true,
161 defaultValue: null,
159 validate: { 162 validate: {
160 descriptionValid: value => { 163 descriptionValid: value => {
161 const res = isVideoDescriptionValid(value) 164 const res = isVideoDescriptionValid(value)
@@ -486,7 +489,7 @@ toFormattedJSON = function (this: VideoInstance) {
486 description: this.getTruncatedDescription(), 489 description: this.getTruncatedDescription(),
487 serverHost, 490 serverHost,
488 isLocal: this.isOwned(), 491 isLocal: this.isOwned(),
489 account: this.VideoChannel.Account.name, 492 accountName: this.VideoChannel.Account.name,
490 duration: this.duration, 493 duration: this.duration,
491 views: this.views, 494 views: this.views,
492 likes: this.likes, 495 likes: this.likes,
@@ -514,6 +517,7 @@ toFormattedDetailsJSON = function (this: VideoInstance) {
514 privacy: this.privacy, 517 privacy: this.privacy,
515 descriptionPath: this.getDescriptionPath(), 518 descriptionPath: this.getDescriptionPath(),
516 channel: this.VideoChannel.toFormattedJSON(), 519 channel: this.VideoChannel.toFormattedJSON(),
520 account: this.VideoChannel.Account.toFormattedJSON(),
517 files: [] 521 files: []
518 } 522 }
519 523
@@ -560,6 +564,22 @@ toActivityPubObject = function (this: VideoInstance) {
560 } 564 }
561 } 565 }
562 566
567 let category
568 if (this.category) {
569 category = {
570 identifier: this.category + '',
571 name: this.getCategoryLabel()
572 }
573 }
574
575 let licence
576 if (this.licence) {
577 licence = {
578 identifier: this.licence + '',
579 name: this.getLicenceLabel()
580 }
581 }
582
563 let likesObject 583 let likesObject
564 let dislikesObject 584 let dislikesObject
565 585
@@ -631,14 +651,8 @@ toActivityPubObject = function (this: VideoInstance) {
631 duration: 'PT' + this.duration + 'S', 651 duration: 'PT' + this.duration + 'S',
632 uuid: this.uuid, 652 uuid: this.uuid,
633 tag, 653 tag,
634 category: { 654 category,
635 identifier: this.category + '', 655 licence,
636 name: this.getCategoryLabel()
637 },
638 licence: {
639 identifier: this.licence + '',
640 name: this.getLicenceLabel()
641 },
642 language, 656 language,
643 views: this.views, 657 views: this.views,
644 nsfw: this.nsfw, 658 nsfw: this.nsfw,
@@ -663,6 +677,8 @@ toActivityPubObject = function (this: VideoInstance) {
663} 677}
664 678
665getTruncatedDescription = function (this: VideoInstance) { 679getTruncatedDescription = function (this: VideoInstance) {
680 if (!this.description) return null
681
666 const options = { 682 const options = {
667 length: CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max 683 length: CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max
668 } 684 }
@@ -753,8 +769,6 @@ getDescriptionPath = function (this: VideoInstance) {
753 769
754getCategoryLabel = function (this: VideoInstance) { 770getCategoryLabel = function (this: VideoInstance) {
755 let categoryLabel = VIDEO_CATEGORIES[this.category] 771 let categoryLabel = VIDEO_CATEGORIES[this.category]
756
757 // Maybe our server is not up to date and there are new categories since our version
758 if (!categoryLabel) categoryLabel = 'Misc' 772 if (!categoryLabel) categoryLabel = 'Misc'
759 773
760 return categoryLabel 774 return categoryLabel
@@ -762,15 +776,12 @@ getCategoryLabel = function (this: VideoInstance) {
762 776
763getLicenceLabel = function (this: VideoInstance) { 777getLicenceLabel = function (this: VideoInstance) {
764 let licenceLabel = VIDEO_LICENCES[this.licence] 778 let licenceLabel = VIDEO_LICENCES[this.licence]
765
766 // Maybe our server is not up to date and there are new licences since our version
767 if (!licenceLabel) licenceLabel = 'Unknown' 779 if (!licenceLabel) licenceLabel = 'Unknown'
768 780
769 return licenceLabel 781 return licenceLabel
770} 782}
771 783
772getLanguageLabel = function (this: VideoInstance) { 784getLanguageLabel = function (this: VideoInstance) {
773 // Language is an optional attribute
774 let languageLabel = VIDEO_LANGUAGES[this.language] 785 let languageLabel = VIDEO_LANGUAGES[this.language]
775 if (!languageLabel) languageLabel = 'Unknown' 786 if (!languageLabel) languageLabel = 'Unknown'
776 787
@@ -1070,7 +1081,7 @@ loadByUUIDAndPopulateAccountAndServerAndTags = function (uuid: string) {
1070 return Video.findOne(options) 1081 return Video.findOne(options)
1071} 1082}
1072 1083
1073searchAndPopulateAccountAndServerAndTags = function (value: string, field: string, start: number, count: number, sort: string) { 1084searchAndPopulateAccountAndServerAndTags = function (value: string, start: number, count: number, sort: string) {
1074 const serverInclude: Sequelize.IncludeOptions = { 1085 const serverInclude: Sequelize.IncludeOptions = {
1075 model: Video['sequelize'].models.Server, 1086 model: Video['sequelize'].models.Server,
1076 required: false 1087 required: false
@@ -1099,33 +1110,24 @@ searchAndPopulateAccountAndServerAndTags = function (value: string, field: strin
1099 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ] 1110 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ]
1100 } 1111 }
1101 1112
1102 if (field === 'tags') { 1113 // TODO: search on tags too
1103 const escapedValue = Video['sequelize'].escape('%' + value + '%') 1114 // const escapedValue = Video['sequelize'].escape('%' + value + '%')
1104 query.where['id'][Sequelize.Op.in] = Video['sequelize'].literal( 1115 // query.where['id'][Sequelize.Op.in] = Video['sequelize'].literal(
1105 `(SELECT "VideoTags"."videoId" 1116 // `(SELECT "VideoTags"."videoId"
1106 FROM "Tags" 1117 // FROM "Tags"
1107 INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" 1118 // INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId"
1108 WHERE name ILIKE ${escapedValue} 1119 // WHERE name ILIKE ${escapedValue}
1109 )` 1120 // )`
1110 ) 1121 // )
1111 } else if (field === 'host') { 1122
1112 // FIXME: Include our server? (not stored in the database) 1123 // TODO: search on account too
1113 serverInclude.where = { 1124 // accountInclude.where = {
1114 host: { 1125 // name: {
1115 [Sequelize.Op.iLike]: '%' + value + '%' 1126 // [Sequelize.Op.iLike]: '%' + value + '%'
1116 } 1127 // }
1117 } 1128 // }
1118 serverInclude.required = true 1129 query.where['name'] = {
1119 } else if (field === 'account') { 1130 [Sequelize.Op.iLike]: '%' + value + '%'
1120 accountInclude.where = {
1121 name: {
1122 [Sequelize.Op.iLike]: '%' + value + '%'
1123 }
1124 }
1125 } else {
1126 query.where[field] = {
1127 [Sequelize.Op.iLike]: '%' + value + '%'
1128 }
1129 } 1131 }
1130 1132
1131 query.include = [ 1133 query.include = [
diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts
index 2962f5640..0aaa6e7c9 100644
--- a/server/tests/api/check-params/videos.ts
+++ b/server/tests/api/check-params/videos.ts
@@ -189,14 +189,6 @@ describe('Test videos API validator', function () {
189 await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) 189 await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
190 }) 190 })
191 191
192 it('Should fail without a category', async function () {
193 const fields = getCompleteVideoUploadAttributes()
194 delete fields.category
195
196 const attaches = getVideoUploadAttaches
197 await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
198 })
199
200 it('Should fail with a bad category', async function () { 192 it('Should fail with a bad category', async function () {
201 const fields = getCompleteVideoUploadAttributes() 193 const fields = getCompleteVideoUploadAttributes()
202 fields.category = 125 194 fields.category = 125
@@ -205,14 +197,6 @@ describe('Test videos API validator', function () {
205 await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) 197 await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
206 }) 198 })
207 199
208 it('Should fail without a licence', async function () {
209 const fields = getCompleteVideoUploadAttributes()
210 delete fields.licence
211
212 const attaches = getVideoUploadAttaches()
213 await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
214 })
215
216 it('Should fail with a bad licence', async function () { 200 it('Should fail with a bad licence', async function () {
217 const fields = getCompleteVideoUploadAttributes() 201 const fields = getCompleteVideoUploadAttributes()
218 fields.licence = 125 202 fields.licence = 125
@@ -245,14 +229,6 @@ describe('Test videos API validator', function () {
245 await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) 229 await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
246 }) 230 })
247 231
248 it('Should fail without description', async function () {
249 const fields = getCompleteVideoUploadAttributes()
250 delete fields.description
251
252 const attaches = getVideoUploadAttaches()
253 await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
254 })
255
256 it('Should fail with a long description', async function () { 232 it('Should fail with a long description', async function () {
257 const fields = getCompleteVideoUploadAttributes() 233 const fields = getCompleteVideoUploadAttributes()
258 fields.description = 'my super description which is very very very very very very very very very very very very long'.repeat(35) 234 fields.description = 'my super description which is very very very very very very very very very very very very long'.repeat(35)
@@ -345,7 +321,7 @@ describe('Test videos API validator', function () {
345 token: server.accessToken, 321 token: server.accessToken,
346 fields, 322 fields,
347 attaches, 323 attaches,
348 statusCodeExpected: 204 324 statusCodeExpected: 200
349 }) 325 })
350 326
351 attaches.videofile = join(__dirname, '..', 'fixtures', 'video_short.mp4') 327 attaches.videofile = join(__dirname, '..', 'fixtures', 'video_short.mp4')
@@ -355,7 +331,7 @@ describe('Test videos API validator', function () {
355 token: server.accessToken, 331 token: server.accessToken,
356 fields, 332 fields,
357 attaches, 333 attaches,
358 statusCodeExpected: 204 334 statusCodeExpected: 200
359 }) 335 })
360 336
361 attaches.videofile = join(__dirname, '..', 'fixtures', 'video_short.ogv') 337 attaches.videofile = join(__dirname, '..', 'fixtures', 'video_short.ogv')
@@ -365,7 +341,7 @@ describe('Test videos API validator', function () {
365 token: server.accessToken, 341 token: server.accessToken,
366 fields, 342 fields,
367 attaches, 343 attaches,
368 statusCodeExpected: 204 344 statusCodeExpected: 200
369 }) 345 })
370 }) 346 })
371 }) 347 })
diff --git a/server/tests/api/follows.ts b/server/tests/api/follows.ts
index aadae3cce..dcb4c8bd9 100644
--- a/server/tests/api/follows.ts
+++ b/server/tests/api/follows.ts
@@ -227,7 +227,7 @@ describe('Test follows', function () {
227 expect(videoDetails.nsfw).to.be.ok 227 expect(videoDetails.nsfw).to.be.ok
228 expect(videoDetails.description).to.equal('my super description') 228 expect(videoDetails.description).to.equal('my super description')
229 expect(videoDetails.serverHost).to.equal('localhost:9003') 229 expect(videoDetails.serverHost).to.equal('localhost:9003')
230 expect(videoDetails.account).to.equal('root') 230 expect(videoDetails.accountName).to.equal('root')
231 expect(videoDetails.likes).to.equal(1) 231 expect(videoDetails.likes).to.equal(1)
232 expect(videoDetails.dislikes).to.equal(1) 232 expect(videoDetails.dislikes).to.equal(1)
233 expect(videoDetails.isLocal).to.be.false 233 expect(videoDetails.isLocal).to.be.false
diff --git a/server/tests/api/multiple-servers.ts b/server/tests/api/multiple-servers.ts
index c80ded862..2f17f017a 100644
--- a/server/tests/api/multiple-servers.ts
+++ b/server/tests/api/multiple-servers.ts
@@ -2,6 +2,8 @@
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { join } from 'path'
6import * as request from 'supertest'
5 7
6import { 8import {
7 dateIsValid, 9 dateIsValid,
@@ -111,13 +113,14 @@ describe('Test multiple servers', function () {
111 expect(video.tags).to.deep.equal([ 'tag1p1', 'tag2p1' ]) 113 expect(video.tags).to.deep.equal([ 'tag1p1', 'tag2p1' ])
112 expect(dateIsValid(video.createdAt)).to.be.true 114 expect(dateIsValid(video.createdAt)).to.be.true
113 expect(dateIsValid(video.updatedAt)).to.be.true 115 expect(dateIsValid(video.updatedAt)).to.be.true
114 expect(video.account).to.equal('root') 116 expect(video.accountName).to.equal('root')
115 117
116 const res2 = await getVideo(server.url, video.uuid) 118 const res2 = await getVideo(server.url, video.uuid)
117 const videoDetails = res2.body 119 const videoDetails = res2.body
118 120
119 expect(videoDetails.channel.name).to.equal('my channel') 121 expect(videoDetails.channel.name).to.equal('my channel')
120 expect(videoDetails.channel.description).to.equal('super channel') 122 expect(videoDetails.channel.description).to.equal('super channel')
123 expect(videoDetails.account.name).to.equal('root')
121 expect(dateIsValid(videoDetails.channel.createdAt)).to.be.true 124 expect(dateIsValid(videoDetails.channel.createdAt)).to.be.true
122 expect(dateIsValid(videoDetails.channel.updatedAt)).to.be.true 125 expect(dateIsValid(videoDetails.channel.updatedAt)).to.be.true
123 expect(videoDetails.files).to.have.lengthOf(1) 126 expect(videoDetails.files).to.have.lengthOf(1)
@@ -201,7 +204,7 @@ describe('Test multiple servers', function () {
201 expect(video.tags).to.deep.equal([ 'tag1p2', 'tag2p2', 'tag3p2' ]) 204 expect(video.tags).to.deep.equal([ 'tag1p2', 'tag2p2', 'tag3p2' ])
202 expect(dateIsValid(video.createdAt)).to.be.true 205 expect(dateIsValid(video.createdAt)).to.be.true
203 expect(dateIsValid(video.updatedAt)).to.be.true 206 expect(dateIsValid(video.updatedAt)).to.be.true
204 expect(video.account).to.equal('user1') 207 expect(video.accountName).to.equal('user1')
205 208
206 if (server.url !== 'http://localhost:9002') { 209 if (server.url !== 'http://localhost:9002') {
207 expect(video.isLocal).to.be.false 210 expect(video.isLocal).to.be.false
@@ -316,7 +319,7 @@ describe('Test multiple servers', function () {
316 expect(video1.serverHost).to.equal('localhost:9003') 319 expect(video1.serverHost).to.equal('localhost:9003')
317 expect(video1.duration).to.equal(5) 320 expect(video1.duration).to.equal(5)
318 expect(video1.tags).to.deep.equal([ 'tag1p3' ]) 321 expect(video1.tags).to.deep.equal([ 'tag1p3' ])
319 expect(video1.account).to.equal('root') 322 expect(video1.accountName).to.equal('root')
320 expect(dateIsValid(video1.createdAt)).to.be.true 323 expect(dateIsValid(video1.createdAt)).to.be.true
321 expect(dateIsValid(video1.updatedAt)).to.be.true 324 expect(dateIsValid(video1.updatedAt)).to.be.true
322 325
@@ -342,7 +345,7 @@ describe('Test multiple servers', function () {
342 expect(video2.serverHost).to.equal('localhost:9003') 345 expect(video2.serverHost).to.equal('localhost:9003')
343 expect(video2.duration).to.equal(5) 346 expect(video2.duration).to.equal(5)
344 expect(video2.tags).to.deep.equal([ 'tag2p3', 'tag3p3', 'tag4p3' ]) 347 expect(video2.tags).to.deep.equal([ 'tag2p3', 'tag3p3', 'tag4p3' ])
345 expect(video2.account).to.equal('root') 348 expect(video2.accountName).to.equal('root')
346 expect(dateIsValid(video2.createdAt)).to.be.true 349 expect(dateIsValid(video2.createdAt)).to.be.true
347 expect(dateIsValid(video2.updatedAt)).to.be.true 350 expect(dateIsValid(video2.updatedAt)).to.be.true
348 351
@@ -690,7 +693,7 @@ describe('Test multiple servers', function () {
690 expect(baseVideo.licence).to.equal(video.licence) 693 expect(baseVideo.licence).to.equal(video.licence)
691 expect(baseVideo.category).to.equal(video.category) 694 expect(baseVideo.category).to.equal(video.category)
692 expect(baseVideo.nsfw).to.equal(video.nsfw) 695 expect(baseVideo.nsfw).to.equal(video.nsfw)
693 expect(baseVideo.account).to.equal(video.account) 696 expect(baseVideo.accountName).to.equal(video.accountName)
694 expect(baseVideo.tags).to.deep.equal(video.tags) 697 expect(baseVideo.tags).to.deep.equal(video.tags)
695 } 698 }
696 }) 699 })
@@ -706,6 +709,50 @@ describe('Test multiple servers', function () {
706 }) 709 })
707 }) 710 })
708 711
712 describe('With minimum parameters', function () {
713 it('Should upload and propagate the video', async function () {
714 this.timeout(50000)
715
716 const path = '/api/v1/videos/upload'
717
718 const req = request(servers[1].url)
719 .post(path)
720 .set('Accept', 'application/json')
721 .set('Authorization', 'Bearer ' + servers[1].accessToken)
722 .field('name', 'minimum parameters')
723 .field('privacy', '1')
724 .field('nsfw', 'false')
725 .field('channelId', '1')
726
727 const filePath = join(__dirname, '..', 'api', 'fixtures', 'video_short.webm')
728
729 await req.attach('videofile', filePath)
730 .expect(200)
731
732 await wait(25000)
733
734 for (const server of servers) {
735 const res = await getVideosList(server.url)
736 const video = res.body.data.find(v => v.name === 'minimum parameters')
737
738 expect(video.name).to.equal('minimum parameters')
739 expect(video.category).to.equal(null)
740 expect(video.categoryLabel).to.equal('Misc')
741 expect(video.licence).to.equal(null)
742 expect(video.licenceLabel).to.equal('Unknown')
743 expect(video.language).to.equal(null)
744 expect(video.languageLabel).to.equal('Unknown')
745 expect(video.nsfw).to.not.be.ok
746 expect(video.description).to.equal(null)
747 expect(video.serverHost).to.equal('localhost:9002')
748 expect(video.accountName).to.equal('root')
749 expect(video.tags).to.deep.equal([ ])
750 expect(dateIsValid(video.createdAt)).to.be.true
751 expect(dateIsValid(video.updatedAt)).to.be.true
752 }
753 })
754 })
755
709 after(async function () { 756 after(async function () {
710 killallServers(servers) 757 killallServers(servers)
711 758
diff --git a/server/tests/api/services.ts b/server/tests/api/services.ts
index 8d96ccc5e..4d480c305 100644
--- a/server/tests/api/services.ts
+++ b/server/tests/api/services.ts
@@ -46,7 +46,7 @@ describe('Test services', function () {
46 46
47 expect(res.body.html).to.equal(expectedHtml) 47 expect(res.body.html).to.equal(expectedHtml)
48 expect(res.body.title).to.equal(server.video.name) 48 expect(res.body.title).to.equal(server.video.name)
49 expect(res.body.author_name).to.equal(server.video.account) 49 expect(res.body.author_name).to.equal(server.video.accountName)
50 expect(res.body.width).to.equal(560) 50 expect(res.body.width).to.equal(560)
51 expect(res.body.height).to.equal(315) 51 expect(res.body.height).to.equal(315)
52 expect(res.body.thumbnail_url).to.equal(expectedThumbnailUrl) 52 expect(res.body.thumbnail_url).to.equal(expectedThumbnailUrl)
@@ -66,7 +66,7 @@ describe('Test services', function () {
66 66
67 expect(res.body.html).to.equal(expectedHtml) 67 expect(res.body.html).to.equal(expectedHtml)
68 expect(res.body.title).to.equal(server.video.name) 68 expect(res.body.title).to.equal(server.video.name)
69 expect(res.body.author_name).to.equal(server.video.account) 69 expect(res.body.author_name).to.equal(server.video.accountName)
70 expect(res.body.height).to.equal(50) 70 expect(res.body.height).to.equal(50)
71 expect(res.body.width).to.equal(50) 71 expect(res.body.width).to.equal(50)
72 expect(res.body).to.not.have.property('thumbnail_url') 72 expect(res.body).to.not.have.property('thumbnail_url')
diff --git a/server/tests/api/single-server.ts b/server/tests/api/single-server.ts
index 041d13225..174fb480d 100644
--- a/server/tests/api/single-server.ts
+++ b/server/tests/api/single-server.ts
@@ -1,40 +1,40 @@
1/* tslint:disable:no-unused-expression */ 1/* tslint:disable:no-unused-expression */
2 2
3import * as chai from 'chai'
3import { keyBy } from 'lodash' 4import { keyBy } from 'lodash'
4import { join } from 'path'
5import 'mocha' 5import 'mocha'
6import * as chai from 'chai' 6import { join } from 'path'
7const expect = chai.expect
8
9import { 7import {
10 ServerInfo,
11 flushTests,
12 runServer,
13 uploadVideo,
14 getVideosList,
15 rateVideo,
16 removeVideo,
17 wait,
18 setAccessTokensToServers,
19 searchVideo,
20 killallServers,
21 dateIsValid, 8 dateIsValid,
9 flushTests,
10 getVideo,
22 getVideoCategories, 11 getVideoCategories,
23 getVideoLicences,
24 getVideoLanguages, 12 getVideoLanguages,
13 getVideoLicences,
25 getVideoPrivacies, 14 getVideoPrivacies,
26 testVideoImage, 15 getVideosList,
27 webtorrentAdd,
28 getVideo,
29 readdirPromise,
30 getVideosListPagination, 16 getVideosListPagination,
31 searchVideoWithPagination,
32 getVideosListSort, 17 getVideosListSort,
18 killallServers,
19 rateVideo,
20 readdirPromise,
21 removeVideo,
22 runServer,
23 searchVideo,
24 searchVideoWithPagination,
33 searchVideoWithSort, 25 searchVideoWithSort,
34 updateVideo 26 ServerInfo,
27 setAccessTokensToServers,
28 testVideoImage,
29 updateVideo,
30 uploadVideo,
31 wait,
32 webtorrentAdd
35} from '../utils' 33} from '../utils'
36import { viewVideo } from '../utils/videos' 34import { viewVideo } from '../utils/videos'
37 35
36const expect = chai.expect
37
38describe('Test a single server', function () { 38describe('Test a single server', function () {
39 let server: ServerInfo = null 39 let server: ServerInfo = null
40 let videoId = -1 40 let videoId = -1
@@ -103,7 +103,10 @@ describe('Test a single server', function () {
103 licence: 6, 103 licence: 6,
104 tags: [ 'tag1', 'tag2', 'tag3' ] 104 tags: [ 'tag1', 'tag2', 'tag3' ]
105 } 105 }
106 await uploadVideo(server.url, server.accessToken, videoAttributes) 106 const res = await uploadVideo(server.url, server.accessToken, videoAttributes)
107 expect(res.body.video).to.not.be.undefined
108 expect(res.body.video.id).to.equal(1)
109 expect(res.body.video.uuid).to.have.length.above(5)
107 }) 110 })
108 111
109 it('Should seed the uploaded video', async function () { 112 it('Should seed the uploaded video', async function () {
@@ -127,7 +130,7 @@ describe('Test a single server', function () {
127 expect(video.nsfw).to.be.ok 130 expect(video.nsfw).to.be.ok
128 expect(video.description).to.equal('my super description') 131 expect(video.description).to.equal('my super description')
129 expect(video.serverHost).to.equal('localhost:9001') 132 expect(video.serverHost).to.equal('localhost:9001')
130 expect(video.account).to.equal('root') 133 expect(video.accountName).to.equal('root')
131 expect(video.isLocal).to.be.true 134 expect(video.isLocal).to.be.true
132 expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) 135 expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
133 expect(dateIsValid(video.createdAt)).to.be.true 136 expect(dateIsValid(video.createdAt)).to.be.true
@@ -176,7 +179,7 @@ describe('Test a single server', function () {
176 expect(video.nsfw).to.be.ok 179 expect(video.nsfw).to.be.ok
177 expect(video.description).to.equal('my super description') 180 expect(video.description).to.equal('my super description')
178 expect(video.serverHost).to.equal('localhost:9001') 181 expect(video.serverHost).to.equal('localhost:9001')
179 expect(video.account).to.equal('root') 182 expect(video.accountName).to.equal('root')
180 expect(video.isLocal).to.be.true 183 expect(video.isLocal).to.be.true
181 expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) 184 expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
182 expect(dateIsValid(video.createdAt)).to.be.true 185 expect(dateIsValid(video.createdAt)).to.be.true
@@ -225,7 +228,7 @@ describe('Test a single server', function () {
225 expect(video.views).to.equal(3) 228 expect(video.views).to.equal(3)
226 }) 229 })
227 230
228 it('Should search the video by name by default', async function () { 231 it('Should search the video by name', async function () {
229 const res = await searchVideo(server.url, 'my') 232 const res = await searchVideo(server.url, 'my')
230 233
231 expect(res.body.total).to.equal(1) 234 expect(res.body.total).to.equal(1)
@@ -243,7 +246,7 @@ describe('Test a single server', function () {
243 expect(video.nsfw).to.be.ok 246 expect(video.nsfw).to.be.ok
244 expect(video.description).to.equal('my super description') 247 expect(video.description).to.equal('my super description')
245 expect(video.serverHost).to.equal('localhost:9001') 248 expect(video.serverHost).to.equal('localhost:9001')
246 expect(video.account).to.equal('root') 249 expect(video.accountName).to.equal('root')
247 expect(video.isLocal).to.be.true 250 expect(video.isLocal).to.be.true
248 expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) 251 expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
249 expect(dateIsValid(video.createdAt)).to.be.true 252 expect(dateIsValid(video.createdAt)).to.be.true
@@ -279,35 +282,36 @@ describe('Test a single server', function () {
279 // }) 282 // })
280 // }) 283 // })
281 284
282 it('Should search the video by tag', async function () { 285 // Not implemented yet
283 const res = await searchVideo(server.url, 'tag1', 'tags') 286 // it('Should search the video by tag', async function () {
284 287 // const res = await searchVideo(server.url, 'tag1')
285 expect(res.body.total).to.equal(1) 288 //
286 expect(res.body.data).to.be.an('array') 289 // expect(res.body.total).to.equal(1)
287 expect(res.body.data.length).to.equal(1) 290 // expect(res.body.data).to.be.an('array')
288 291 // expect(res.body.data.length).to.equal(1)
289 const video = res.body.data[0] 292 //
290 expect(video.name).to.equal('my super name') 293 // const video = res.body.data[0]
291 expect(video.category).to.equal(2) 294 // expect(video.name).to.equal('my super name')
292 expect(video.categoryLabel).to.equal('Films') 295 // expect(video.category).to.equal(2)
293 expect(video.licence).to.equal(6) 296 // expect(video.categoryLabel).to.equal('Films')
294 expect(video.licenceLabel).to.equal('Attribution - Non Commercial - No Derivatives') 297 // expect(video.licence).to.equal(6)
295 expect(video.language).to.equal(3) 298 // expect(video.licenceLabel).to.equal('Attribution - Non Commercial - No Derivatives')
296 expect(video.languageLabel).to.equal('Mandarin') 299 // expect(video.language).to.equal(3)
297 expect(video.nsfw).to.be.ok 300 // expect(video.languageLabel).to.equal('Mandarin')
298 expect(video.description).to.equal('my super description') 301 // expect(video.nsfw).to.be.ok
299 expect(video.serverHost).to.equal('localhost:9001') 302 // expect(video.description).to.equal('my super description')
300 expect(video.account).to.equal('root') 303 // expect(video.serverHost).to.equal('localhost:9001')
301 expect(video.isLocal).to.be.true 304 // expect(video.accountName).to.equal('root')
302 expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) 305 // expect(video.isLocal).to.be.true
303 expect(dateIsValid(video.createdAt)).to.be.true 306 // expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
304 expect(dateIsValid(video.updatedAt)).to.be.true 307 // expect(dateIsValid(video.createdAt)).to.be.true
305 308 // expect(dateIsValid(video.updatedAt)).to.be.true
306 const test = await testVideoImage(server.url, 'video_short.webm', video.thumbnailPath) 309 //
307 expect(test).to.equal(true) 310 // const test = await testVideoImage(server.url, 'video_short.webm', video.thumbnailPath)
308 }) 311 // expect(test).to.equal(true)
312 // })
309 313
310 it('Should not find a search by name by default', async function () { 314 it('Should not find a search by name', async function () {
311 const res = await searchVideo(server.url, 'hello') 315 const res = await searchVideo(server.url, 'hello')
312 316
313 expect(res.body.total).to.equal(0) 317 expect(res.body.total).to.equal(0)
@@ -315,21 +319,23 @@ describe('Test a single server', function () {
315 expect(res.body.data.length).to.equal(0) 319 expect(res.body.data.length).to.equal(0)
316 }) 320 })
317 321
318 it('Should not find a search by author', async function () { 322 // Not implemented yet
319 const res = await searchVideo(server.url, 'hello', 'account') 323 // it('Should not find a search by author', async function () {
320 324 // const res = await searchVideo(server.url, 'hello')
321 expect(res.body.total).to.equal(0) 325 //
322 expect(res.body.data).to.be.an('array') 326 // expect(res.body.total).to.equal(0)
323 expect(res.body.data.length).to.equal(0) 327 // expect(res.body.data).to.be.an('array')
324 }) 328 // expect(res.body.data.length).to.equal(0)
325 329 // })
326 it('Should not find a search by tag', async function () { 330 //
327 const res = await searchVideo(server.url, 'hello', 'tags') 331 // Not implemented yet
328 332 // it('Should not find a search by tag', async function () {
329 expect(res.body.total).to.equal(0) 333 // const res = await searchVideo(server.url, 'hello')
330 expect(res.body.data).to.be.an('array') 334 //
331 expect(res.body.data.length).to.equal(0) 335 // expect(res.body.total).to.equal(0)
332 }) 336 // expect(res.body.data).to.be.an('array')
337 // expect(res.body.data.length).to.equal(0)
338 // })
333 339
334 it('Should remove the video', async function () { 340 it('Should remove the video', async function () {
335 await removeVideo(server.url, server.accessToken, videoId) 341 await removeVideo(server.url, server.accessToken, videoId)
@@ -357,7 +363,7 @@ describe('Test a single server', function () {
357 'video_short1.webm', 'video_short2.webm', 'video_short3.webm' 363 'video_short1.webm', 'video_short2.webm', 'video_short3.webm'
358 ] 364 ]
359 365
360 // const tasks: Promise<any>[] = [] 366 const tasks: Promise<any>[] = []
361 for (const video of videos) { 367 for (const video of videos) {
362 const videoAttributes = { 368 const videoAttributes = {
363 name: video + ' name', 369 name: video + ' name',
@@ -371,13 +377,10 @@ describe('Test a single server', function () {
371 } 377 }
372 378
373 const p = uploadVideo(server.url, server.accessToken, videoAttributes) 379 const p = uploadVideo(server.url, server.accessToken, videoAttributes)
374 await p 380 tasks.push(p)
375 } 381 }
376 // FIXME: concurrent uploads does not work :( 382
377 // tasks.push(p) 383 await Promise.all(tasks)
378 // }
379 //
380 // await Promise.all(tasks)
381 }) 384 })
382 385
383 it('Should have the correct durations', async function () { 386 it('Should have the correct durations', async function () {
@@ -443,7 +446,7 @@ describe('Test a single server', function () {
443 }) 446 })
444 447
445 it('Should search the first video', async function () { 448 it('Should search the first video', async function () {
446 const res = await searchVideoWithPagination(server.url, 'webm', 'name', 0, 1, 'name') 449 const res = await searchVideoWithPagination(server.url, 'webm', 0, 1, 'name')
447 450
448 const videos = res.body.data 451 const videos = res.body.data
449 expect(res.body.total).to.equal(4) 452 expect(res.body.total).to.equal(4)
@@ -452,7 +455,7 @@ describe('Test a single server', function () {
452 }) 455 })
453 456
454 it('Should search the last two videos', async function () { 457 it('Should search the last two videos', async function () {
455 const res = await searchVideoWithPagination(server.url, 'webm', 'name', 2, 2, 'name') 458 const res = await searchVideoWithPagination(server.url, 'webm', 2, 2, 'name')
456 459
457 const videos = res.body.data 460 const videos = res.body.data
458 expect(res.body.total).to.equal(4) 461 expect(res.body.total).to.equal(4)
@@ -462,20 +465,21 @@ describe('Test a single server', function () {
462 }) 465 })
463 466
464 it('Should search all the webm videos', async function () { 467 it('Should search all the webm videos', async function () {
465 const res = await searchVideoWithPagination(server.url, 'webm', 'name', 0, 15) 468 const res = await searchVideoWithPagination(server.url, 'webm', 0, 15)
466 469
467 const videos = res.body.data 470 const videos = res.body.data
468 expect(res.body.total).to.equal(4) 471 expect(res.body.total).to.equal(4)
469 expect(videos.length).to.equal(4) 472 expect(videos.length).to.equal(4)
470 }) 473 })
471 474
472 it('Should search all the root author videos', async function () { 475 // Not implemented yet
473 const res = await searchVideoWithPagination(server.url, 'root', 'account', 0, 15) 476 // it('Should search all the root author videos', async function () {
474 477 // const res = await searchVideoWithPagination(server.url, 'root', 0, 15)
475 const videos = res.body.data 478 //
476 expect(res.body.total).to.equal(6) 479 // const videos = res.body.data
477 expect(videos.length).to.equal(6) 480 // expect(res.body.total).to.equal(6)
478 }) 481 // expect(videos.length).to.equal(6)
482 // })
479 483
480 // Not implemented yet 484 // Not implemented yet
481 // it('Should search all the 9001 port videos', async function () { 485 // it('Should search all the 9001 port videos', async function () {
@@ -559,7 +563,8 @@ describe('Test a single server', function () {
559 expect(video.nsfw).to.be.ok 563 expect(video.nsfw).to.be.ok
560 expect(video.description).to.equal('my super description updated') 564 expect(video.description).to.equal('my super description updated')
561 expect(video.serverHost).to.equal('localhost:9001') 565 expect(video.serverHost).to.equal('localhost:9001')
562 expect(video.account).to.equal('root') 566 expect(video.accountName).to.equal('root')
567 expect(video.account.name).to.equal('root')
563 expect(video.isLocal).to.be.true 568 expect(video.isLocal).to.be.true
564 expect(video.tags).to.deep.equal([ 'tagup1', 'tagup2' ]) 569 expect(video.tags).to.deep.equal([ 'tagup1', 'tagup2' ])
565 expect(dateIsValid(video.createdAt)).to.be.true 570 expect(dateIsValid(video.createdAt)).to.be.true
@@ -608,7 +613,7 @@ describe('Test a single server', function () {
608 expect(video.nsfw).to.be.ok 613 expect(video.nsfw).to.be.ok
609 expect(video.description).to.equal('my super description updated') 614 expect(video.description).to.equal('my super description updated')
610 expect(video.serverHost).to.equal('localhost:9001') 615 expect(video.serverHost).to.equal('localhost:9001')
611 expect(video.account).to.equal('root') 616 expect(video.accountName).to.equal('root')
612 expect(video.isLocal).to.be.true 617 expect(video.isLocal).to.be.true
613 expect(video.tags).to.deep.equal([ 'supertag', 'tag1', 'tag2' ]) 618 expect(video.tags).to.deep.equal([ 'supertag', 'tag1', 'tag2' ])
614 expect(dateIsValid(video.createdAt)).to.be.true 619 expect(dateIsValid(video.createdAt)).to.be.true
@@ -648,7 +653,7 @@ describe('Test a single server', function () {
648 expect(video.nsfw).to.be.ok 653 expect(video.nsfw).to.be.ok
649 expect(video.description).to.equal('hello everybody') 654 expect(video.description).to.equal('hello everybody')
650 expect(video.serverHost).to.equal('localhost:9001') 655 expect(video.serverHost).to.equal('localhost:9001')
651 expect(video.account).to.equal('root') 656 expect(video.accountName).to.equal('root')
652 expect(video.isLocal).to.be.true 657 expect(video.isLocal).to.be.true
653 expect(video.tags).to.deep.equal([ 'supertag', 'tag1', 'tag2' ]) 658 expect(video.tags).to.deep.equal([ 'supertag', 'tag1', 'tag2' ])
654 expect(dateIsValid(video.createdAt)).to.be.true 659 expect(dateIsValid(video.createdAt)).to.be.true
diff --git a/server/tests/api/users.ts b/server/tests/api/users.ts
index 33646e84f..b3163b1e1 100644
--- a/server/tests/api/users.ts
+++ b/server/tests/api/users.ts
@@ -113,11 +113,11 @@ describe('Test users', function () {
113 113
114 it('Should upload the video with the correct token', async function () { 114 it('Should upload the video with the correct token', async function () {
115 const videoAttributes = {} 115 const videoAttributes = {}
116 await uploadVideo(server.url, accessToken, videoAttributes, 204) 116 await uploadVideo(server.url, accessToken, videoAttributes)
117 const res = await getVideosList(server.url) 117 const res = await getVideosList(server.url)
118 const video = res.body.data[ 0 ] 118 const video = res.body.data[ 0 ]
119 119
120 expect(video.account) 120 expect(video.accountName)
121 .to 121 .to
122 .equal('root') 122 .equal('root')
123 videoId = video.id 123 videoId = video.id
@@ -125,7 +125,7 @@ describe('Test users', function () {
125 125
126 it('Should upload the video again with the correct token', async function () { 126 it('Should upload the video again with the correct token', async function () {
127 const videoAttributes = {} 127 const videoAttributes = {}
128 await uploadVideo(server.url, accessToken, videoAttributes, 204) 128 await uploadVideo(server.url, accessToken, videoAttributes)
129 }) 129 })
130 130
131 it('Should retrieve a video rating', async function () { 131 it('Should retrieve a video rating', async function () {
@@ -487,7 +487,7 @@ describe('Test users', function () {
487 .equal(1) 487 .equal(1)
488 488
489 const video = res.body.data[ 0 ] 489 const video = res.body.data[ 0 ]
490 expect(video.account) 490 expect(video.accountName)
491 .to 491 .to
492 .equal('root') 492 .equal('root')
493 }) 493 })
diff --git a/server/tests/utils/servers.ts b/server/tests/utils/servers.ts
index faa2f19ff..8340fbc18 100644
--- a/server/tests/utils/servers.ts
+++ b/server/tests/utils/servers.ts
@@ -24,7 +24,7 @@ interface ServerInfo {
24 id: number 24 id: number
25 uuid: string 25 uuid: string
26 name: string 26 name: string
27 account: string 27 accountName: string
28 } 28 }
29 29
30 remoteVideo?: { 30 remoteVideo?: {
diff --git a/server/tests/utils/videos.ts b/server/tests/utils/videos.ts
index 73a9f1a0a..fb758cf29 100644
--- a/server/tests/utils/videos.ts
+++ b/server/tests/utils/videos.ts
@@ -145,26 +145,25 @@ function removeVideo (url: string, token: string, id: number, expectedStatus = 2
145 .expect(expectedStatus) 145 .expect(expectedStatus)
146} 146}
147 147
148function searchVideo (url: string, search: string, field?: string) { 148function searchVideo (url: string, search: string) {
149 const path = '/api/v1/videos' 149 const path = '/api/v1/videos'
150 const req = request(url) 150 const req = request(url)
151 .get(path + '/search/' + search) 151 .get(path + '/search')
152 .set('Accept', 'application/json') 152 .query({ search })
153 153 .set('Accept', 'application/json')
154 if (field) req.query({ field })
155 154
156 return req.expect(200) 155 return req.expect(200)
157 .expect('Content-Type', /json/) 156 .expect('Content-Type', /json/)
158} 157}
159 158
160function searchVideoWithPagination (url: string, search: string, field: string, start: number, count: number, sort?: string) { 159function searchVideoWithPagination (url: string, search: string, start: number, count: number, sort?: string) {
161 const path = '/api/v1/videos' 160 const path = '/api/v1/videos'
162 161
163 const req = request(url) 162 const req = request(url)
164 .get(path + '/search/' + search) 163 .get(path + '/search')
165 .query({ start }) 164 .query({ start })
165 .query({ search })
166 .query({ count }) 166 .query({ count })
167 .query({ field })
168 167
169 if (sort) req.query({ sort }) 168 if (sort) req.query({ sort })
170 169
@@ -177,7 +176,8 @@ function searchVideoWithSort (url: string, search: string, sort: string) {
177 const path = '/api/v1/videos' 176 const path = '/api/v1/videos'
178 177
179 return request(url) 178 return request(url)
180 .get(path + '/search/' + search) 179 .get(path + '/search')
180 .query({ search })
181 .query({ sort }) 181 .query({ sort })
182 .set('Accept', 'application/json') 182 .set('Accept', 'application/json')
183 .expect(200) 183 .expect(200)
@@ -201,7 +201,7 @@ async function testVideoImage (url: string, imageName: string, imagePath: string
201 } 201 }
202} 202}
203 203
204async function uploadVideo (url: string, accessToken: string, videoAttributesArg: VideoAttributes, specialStatus = 204) { 204async function uploadVideo (url: string, accessToken: string, videoAttributesArg: VideoAttributes, specialStatus = 200) {
205 const path = '/api/v1/videos/upload' 205 const path = '/api/v1/videos/upload'
206 let defaultChannelId = '1' 206 let defaultChannelId = '1'
207 207
diff --git a/shared/models/accounts/account.model.ts b/shared/models/accounts/account.model.ts
index 338426dc7..d14701317 100644
--- a/shared/models/accounts/account.model.ts
+++ b/shared/models/accounts/account.model.ts
@@ -1,5 +1,13 @@
1import { Avatar } from '../avatars/avatar.model'
2
1export interface Account { 3export interface Account {
2 id: number 4 id: number
5 uuid: string
3 name: string 6 name: string
4 host: string 7 host: string
8 followingCount: number
9 followersCount: number
10 createdAt: Date
11 updatedAt: Date
12 avatar: Avatar
5} 13}
diff --git a/shared/models/avatars/avatar.model.ts b/shared/models/avatars/avatar.model.ts
new file mode 100644
index 000000000..301d00929
--- /dev/null
+++ b/shared/models/avatars/avatar.model.ts
@@ -0,0 +1,5 @@
1export interface Avatar {
2 path: string
3 createdAt: Date | string
4 updatedAt: Date | string
5}
diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts
index a8012734c..4b17881e5 100644
--- a/shared/models/users/user.model.ts
+++ b/shared/models/users/user.model.ts
@@ -1,3 +1,4 @@
1import { Account } from '../accounts'
1import { VideoChannel } from '../videos/video-channel.model' 2import { VideoChannel } from '../videos/video-channel.model'
2import { UserRole } from './user-role' 3import { UserRole } from './user-role'
3 4
@@ -8,10 +9,7 @@ export interface User {
8 displayNSFW: boolean 9 displayNSFW: boolean
9 role: UserRole 10 role: UserRole
10 videoQuota: number 11 videoQuota: number
11 createdAt: Date, 12 createdAt: Date
12 account: { 13 account: Account
13 id: number
14 uuid: string
15 }
16 videoChannels?: VideoChannel[] 14 videoChannels?: VideoChannel[]
17} 15}
diff --git a/shared/models/videos/video-create.model.ts b/shared/models/videos/video-create.model.ts
index e537c38a8..8bc6a6639 100644
--- a/shared/models/videos/video-create.model.ts
+++ b/shared/models/videos/video-create.model.ts
@@ -1,13 +1,13 @@
1import { VideoPrivacy } from './video-privacy.enum' 1import { VideoPrivacy } from './video-privacy.enum'
2 2
3export interface VideoCreate { 3export interface VideoCreate {
4 category: number 4 category?: number
5 licence: number 5 licence?: number
6 language: number 6 language?: number
7 description: string 7 description?: string
8 channelId: number 8 channelId: number
9 nsfw: boolean 9 nsfw: boolean
10 name: string 10 name: string
11 tags: string[] 11 tags?: string[]
12 privacy: VideoPrivacy 12 privacy: VideoPrivacy
13} 13}
diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts
index 08b29425c..dc12a05d9 100644
--- a/shared/models/videos/video.model.ts
+++ b/shared/models/videos/video.model.ts
@@ -1,3 +1,4 @@
1import { Account } from '../accounts'
1import { VideoChannel } from './video-channel.model' 2import { VideoChannel } from './video-channel.model'
2import { VideoPrivacy } from './video-privacy.enum' 3import { VideoPrivacy } from './video-privacy.enum'
3 4
@@ -13,7 +14,7 @@ export interface VideoFile {
13export interface Video { 14export interface Video {
14 id: number 15 id: number
15 uuid: string 16 uuid: string
16 account: string 17 accountName: string
17 createdAt: Date | string 18 createdAt: Date | string
18 updatedAt: Date | string 19 updatedAt: Date | string
19 categoryLabel: string 20 categoryLabel: string
@@ -43,4 +44,5 @@ export interface VideoDetails extends Video {
43 descriptionPath: string 44 descriptionPath: string
44 channel: VideoChannel 45 channel: VideoChannel
45 files: VideoFile[] 46 files: VideoFile[]
47 account: Account
46} 48}