mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2024-11-24 09:40:28 -06:00
Merge branch 'master' into webseed-merged
This commit is contained in:
commit
a6375e6966
7
.gitignore
vendored
7
.gitignore
vendored
@ -14,4 +14,11 @@ uploads
|
||||
thumbnails
|
||||
config/production.yaml
|
||||
ffmpeg
|
||||
<<<<<<< HEAD
|
||||
torrents
|
||||
=======
|
||||
.tags
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
torrents/
|
||||
>>>>>>> master
|
||||
|
@ -1,8 +1,8 @@
|
||||
language: node_js
|
||||
|
||||
node_js:
|
||||
- "4.4"
|
||||
- "6.2"
|
||||
- "4.5"
|
||||
- "6.6"
|
||||
|
||||
env:
|
||||
- CXX=g++-4.8
|
||||
@ -19,8 +19,10 @@ sudo: false
|
||||
services:
|
||||
- mongodb
|
||||
|
||||
before_install: if [[ `npm -v` != 3* ]]; then npm i -g npm@3; fi
|
||||
|
||||
before_script:
|
||||
- npm install electron-prebuilt -g
|
||||
- npm install electron -g
|
||||
- npm run build
|
||||
- wget --no-check-certificate "https://download.cpy.re/ffmpeg/ffmpeg-release-3.0.2-64bit-static.tar.xz"
|
||||
- tar xf ffmpeg-release-3.0.2-64bit-static.tar.xz
|
||||
|
19
README.md
19
README.md
@ -60,6 +60,7 @@ Want to see in action?
|
||||
|
||||
* You can directly test in your browser with this [demo server](http://peertube.cpy.re). Don't forget to use the latest version of Firefox/Chromium/(Opera?) and check your firewall configuration (for WebRTC)
|
||||
* You can find [a video](https://vimeo.com/164881662 "Yes Vimeo, please don't judge me") to see how the "decentralization feature" looks like
|
||||
* Experimental demo servers that share videos (they are in the same network): [peertube2](http://peertube2.cpy.re), [peertube3](http://peertube3.cpy.re). Since I do experiments with them, sometimes they might not work correctly.
|
||||
|
||||
## Why
|
||||
|
||||
@ -95,10 +96,12 @@ Thanks to [WebTorrent](https://github.com/feross/webtorrent), we can make P2P (t
|
||||
- [ ] Validate the prototype (test PeerTube in a real world with many pods and videos)
|
||||
- [ ] Manage API breaks
|
||||
- [ ] Add "DDOS" security (check if a pod don't send too many requests for example)
|
||||
- [ ] Admin panel
|
||||
- [ ] Stats about the network (how many friends, how many requests per hour...)
|
||||
- [ ] Stats about videos
|
||||
- [ ] Manage users (create/remove)
|
||||
- [X] Admin panel
|
||||
- [X] Stats
|
||||
- [X] Friends list
|
||||
- [X] Manage users (create/remove)
|
||||
- [ ] User playlists
|
||||
- [ ] User subscriptions (by tags, author...)
|
||||
|
||||
|
||||
## Installation
|
||||
@ -111,6 +114,7 @@ Thanks to [WebTorrent](https://github.com/feross/webtorrent), we can make P2P (t
|
||||
### Dependencies
|
||||
|
||||
* **NodeJS >= 4.2**
|
||||
* **npm >= 3.0**
|
||||
* OpenSSL (cli)
|
||||
* MongoDB
|
||||
* ffmpeg xvfb-run libgtk2.0-0 libgconf-2-4 libnss3 libasound2 libxtst6 libxss1 libnotify-bin (for electron)
|
||||
@ -123,7 +127,8 @@ Thanks to [WebTorrent](https://github.com/feross/webtorrent), we can make P2P (t
|
||||
|
||||
# apt-get update
|
||||
# apt-get install ffmpeg mongodb openssl xvfb curl sudo git build-essential libgtk2.0-0 libgconf-2-4 libnss3 libasound2 libxtst6 libxss1 libnotify-bin
|
||||
# npm install -g electron-prebuilt
|
||||
# npm install -g npm@3
|
||||
# npm install -g electron
|
||||
|
||||
#### Other distribution... (PR welcome)
|
||||
|
||||
@ -160,6 +165,10 @@ Finally, run the server with the `production` `NODE_ENV` variable set.
|
||||
|
||||
$ NODE_ENV=production npm start
|
||||
|
||||
**Nginx template** (reverse proxy): https://github.com/Chocobozzz/PeerTube/tree/master/support/nginx
|
||||
|
||||
**Systemd template**: https://github.com/Chocobozzz/PeerTube/tree/master/support/systemd
|
||||
|
||||
### Other commands
|
||||
|
||||
To print all available command run:
|
||||
|
@ -8,10 +8,15 @@ function hasProcessFlag (flag) {
|
||||
return process.argv.join('').indexOf(flag) > -1
|
||||
}
|
||||
|
||||
function isWebpackDevServer () {
|
||||
return process.argv[1] && !!(/webpack-dev-server$/.exec(process.argv[1]))
|
||||
}
|
||||
|
||||
function root (args) {
|
||||
args = Array.prototype.slice.call(arguments, 0)
|
||||
return path.join.apply(path, [ROOT].concat(args))
|
||||
}
|
||||
|
||||
exports.hasProcessFlag = hasProcessFlag
|
||||
exports.isWebpackDevServer = isWebpackDevServer
|
||||
exports.root = root
|
||||
|
@ -5,9 +5,11 @@ const helpers = require('./helpers')
|
||||
* Webpack Plugins
|
||||
*/
|
||||
|
||||
var CopyWebpackPlugin = (CopyWebpackPlugin = require('copy-webpack-plugin'), CopyWebpackPlugin.default || CopyWebpackPlugin)
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const ForkCheckerPlugin = require('awesome-typescript-loader').ForkCheckerPlugin
|
||||
const AssetsPlugin = require('assets-webpack-plugin')
|
||||
const ContextReplacementPlugin = require('webpack/lib/ContextReplacementPlugin')
|
||||
const WebpackNotifierPlugin = require('webpack-notifier')
|
||||
|
||||
/*
|
||||
@ -15,7 +17,8 @@ const WebpackNotifierPlugin = require('webpack-notifier')
|
||||
*/
|
||||
const METADATA = {
|
||||
title: 'PeerTube',
|
||||
baseUrl: '/'
|
||||
baseUrl: '/',
|
||||
isDevServer: helpers.isWebpackDevServer()
|
||||
}
|
||||
|
||||
/*
|
||||
@ -23,7 +26,10 @@ const METADATA = {
|
||||
*
|
||||
* See: http://webpack.github.io/docs/configuration.html#cli
|
||||
*/
|
||||
module.exports = {
|
||||
module.exports = function (options) {
|
||||
var isProd = options.env === 'production'
|
||||
|
||||
return {
|
||||
/*
|
||||
* Static metadata for index.html
|
||||
*
|
||||
@ -69,10 +75,7 @@ module.exports = {
|
||||
root: helpers.root('src'),
|
||||
|
||||
// remove other default values
|
||||
modulesDirectories: [ 'node_modules' ],
|
||||
|
||||
packageAlias: 'browser'
|
||||
|
||||
modulesDirectories: [ 'node_modules' ]
|
||||
},
|
||||
|
||||
output: {
|
||||
@ -91,30 +94,16 @@ module.exports = {
|
||||
* See: http://webpack.github.io/docs/configuration.html#module-preloaders-module-postloaders
|
||||
*/
|
||||
preLoaders: [
|
||||
|
||||
/*
|
||||
* Tslint loader support for *.ts files
|
||||
*
|
||||
* See: https://github.com/wbuchwalter/tslint-loader
|
||||
*/
|
||||
// { test: /\.ts$/, loader: 'tslint-loader', exclude: [ helpers.root('node_modules') ] },
|
||||
|
||||
/*
|
||||
* Source map loader support for *.js files
|
||||
* Extracts SourceMaps for source files that as added as sourceMappingURL comment.
|
||||
*
|
||||
* See: https://github.com/webpack/source-map-loader
|
||||
*/
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'source-map-loader',
|
||||
exclude: [
|
||||
// these packages have problems with their sourcemaps
|
||||
helpers.root('node_modules/rxjs'),
|
||||
helpers.root('node_modules/@angular')
|
||||
]
|
||||
test: /\.ts$/,
|
||||
loader: 'string-replace-loader',
|
||||
query: {
|
||||
search: '(System|SystemJS)(.*[\\n\\r]\\s*\\.|\\.)import\\((.+)\\)',
|
||||
replace: '$1.import($3).then(mod => (mod.__esModule && mod.default) ? mod.default : mod)',
|
||||
flags: 'g'
|
||||
},
|
||||
include: [helpers.root('src')]
|
||||
}
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
@ -134,7 +123,11 @@ module.exports = {
|
||||
*/
|
||||
{
|
||||
test: /\.ts$/,
|
||||
loader: 'awesome-typescript-loader',
|
||||
loaders: [
|
||||
'@angularclass/hmr-loader?pretty=' + !isProd + '&prod=' + isProd,
|
||||
'awesome-typescript-loader',
|
||||
'angular2-template-loader'
|
||||
],
|
||||
exclude: [/\.(spec|e2e)\.ts$/]
|
||||
},
|
||||
|
||||
@ -149,15 +142,11 @@ module.exports = {
|
||||
},
|
||||
|
||||
{
|
||||
test: /\.scss$/,
|
||||
exclude: /node_modules/,
|
||||
loaders: [ 'raw-loader', 'sass-loader' ]
|
||||
},
|
||||
|
||||
{
|
||||
test: /\.(woff2?|ttf|eot|svg)$/,
|
||||
loader: 'url?limit=10000&name=assets/fonts/[hash].[ext]'
|
||||
test: /\.(sass|scss)$/,
|
||||
loaders: ['css-to-string-loader', 'css-loader?sourceMap', 'resolve-url', 'sass-loader?sourceMap']
|
||||
},
|
||||
{ test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'url?limit=10000&minetype=application/font-woff' },
|
||||
{ test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file' },
|
||||
|
||||
/* Raw loader support for *.html
|
||||
* Returns file content as string
|
||||
@ -184,6 +173,11 @@ module.exports = {
|
||||
* See: http://webpack.github.io/docs/configuration.html#plugins
|
||||
*/
|
||||
plugins: [
|
||||
new AssetsPlugin({
|
||||
path: helpers.root('dist'),
|
||||
filename: 'webpack-assets.json',
|
||||
prettyPrint: true
|
||||
}),
|
||||
|
||||
/*
|
||||
* Plugin: ForkCheckerPlugin
|
||||
@ -193,16 +187,6 @@ module.exports = {
|
||||
*/
|
||||
new ForkCheckerPlugin(),
|
||||
|
||||
/*
|
||||
* Plugin: OccurenceOrderPlugin
|
||||
* Description: Varies the distribution of the ids to get the smallest id length
|
||||
* for often used ids.
|
||||
*
|
||||
* See: https://webpack.github.io/docs/list-of-plugins.html#occurrenceorderplugin
|
||||
* See: https://github.com/webpack/docs/wiki/optimization#minimize
|
||||
*/
|
||||
new webpack.optimize.OccurenceOrderPlugin(true),
|
||||
|
||||
/*
|
||||
* Plugin: CommonsChunkPlugin
|
||||
* Description: Shares common code between the pages.
|
||||
@ -215,6 +199,19 @@ module.exports = {
|
||||
name: [ 'polyfills', 'vendor' ].reverse()
|
||||
}),
|
||||
|
||||
/**
|
||||
* Plugin: ContextReplacementPlugin
|
||||
* Description: Provides context to Angular's use of System.import
|
||||
*
|
||||
* See: https://webpack.github.io/docs/list-of-plugins.html#contextreplacementplugin
|
||||
* See: https://github.com/angular/angular/issues/11580
|
||||
*/
|
||||
new ContextReplacementPlugin(
|
||||
// The (\\|\/) piece accounts for path separators in *nix and Windows
|
||||
/angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/,
|
||||
helpers.root('src') // location of your src
|
||||
),
|
||||
|
||||
/*
|
||||
* Plugin: CopyWebpackPlugin
|
||||
* Description: Copy files and directories in webpack.
|
||||
@ -265,5 +262,5 @@ module.exports = {
|
||||
clearImmediate: false,
|
||||
setImmediate: false
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -6,15 +6,18 @@ const commonConfig = require('./webpack.common.js') // the settings that are com
|
||||
* Webpack Plugins
|
||||
*/
|
||||
const DefinePlugin = require('webpack/lib/DefinePlugin')
|
||||
const NamedModulesPlugin = require('webpack/lib/NamedModulesPlugin')
|
||||
|
||||
/**
|
||||
* Webpack Constants
|
||||
*/
|
||||
const ENV = process.env.ENV = process.env.NODE_ENV = 'development'
|
||||
const HOST = process.env.HOST || 'localhost'
|
||||
const PORT = process.env.PORT || 3000
|
||||
const HMR = helpers.hasProcessFlag('hot')
|
||||
const METADATA = webpackMerge(commonConfig.metadata, {
|
||||
host: 'localhost',
|
||||
port: 3000,
|
||||
const METADATA = webpackMerge(commonConfig({env: ENV}).metadata, {
|
||||
host: HOST,
|
||||
port: PORT,
|
||||
ENV: ENV,
|
||||
HMR: HMR
|
||||
})
|
||||
@ -24,7 +27,8 @@ const METADATA = webpackMerge(commonConfig.metadata, {
|
||||
*
|
||||
* See: http://webpack.github.io/docs/configuration.html#cli
|
||||
*/
|
||||
module.exports = webpackMerge(commonConfig, {
|
||||
module.exports = function (env) {
|
||||
return webpackMerge(commonConfig({env: ENV}), {
|
||||
/**
|
||||
* Merged metadata from webpack.common.js for index.html
|
||||
*
|
||||
@ -81,7 +85,10 @@ module.exports = webpackMerge(commonConfig, {
|
||||
*
|
||||
* See: http://webpack.github.io/docs/configuration.html#output-chunkfilename
|
||||
*/
|
||||
chunkFilename: '[id].chunk.js'
|
||||
chunkFilename: '[id].chunk.js',
|
||||
|
||||
library: 'ac_[name]',
|
||||
libraryTarget: 'var'
|
||||
|
||||
},
|
||||
|
||||
@ -109,7 +116,9 @@ module.exports = webpackMerge(commonConfig, {
|
||||
'NODE_ENV': JSON.stringify(METADATA.ENV),
|
||||
'HMR': METADATA.HMR
|
||||
}
|
||||
})
|
||||
}),
|
||||
|
||||
new NamedModulesPlugin()
|
||||
],
|
||||
|
||||
/**
|
||||
@ -124,6 +133,17 @@ module.exports = webpackMerge(commonConfig, {
|
||||
resourcePath: 'src'
|
||||
},
|
||||
|
||||
devServer: {
|
||||
port: METADATA.port,
|
||||
host: METADATA.host,
|
||||
historyApiFallback: true,
|
||||
watchOptions: {
|
||||
aggregateTimeout: 300,
|
||||
poll: 1000
|
||||
},
|
||||
outputPath: helpers.root('dist')
|
||||
},
|
||||
|
||||
/*
|
||||
* Include polyfills or mocks for various node stuff
|
||||
* Description: Node configuration
|
||||
@ -138,5 +158,5 @@ module.exports = webpackMerge(commonConfig, {
|
||||
clearImmediate: false,
|
||||
setImmediate: false
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
@ -9,10 +9,12 @@ const commonConfig = require('./webpack.common.js') // the settings that are com
|
||||
/**
|
||||
* Webpack Plugins
|
||||
*/
|
||||
// const ProvidePlugin = require('webpack/lib/ProvidePlugin')
|
||||
const DefinePlugin = require('webpack/lib/DefinePlugin')
|
||||
const DedupePlugin = require('webpack/lib/optimize/DedupePlugin')
|
||||
const NormalModuleReplacementPlugin = require('webpack/lib/NormalModuleReplacementPlugin')
|
||||
// const IgnorePlugin = require('webpack/lib/IgnorePlugin')
|
||||
// const DedupePlugin = require('webpack/lib/optimize/DedupePlugin')
|
||||
const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin')
|
||||
const CompressionPlugin = require('compression-webpack-plugin')
|
||||
const WebpackMd5Hash = require('webpack-md5-hash')
|
||||
|
||||
/**
|
||||
@ -21,14 +23,15 @@ const WebpackMd5Hash = require('webpack-md5-hash')
|
||||
const ENV = process.env.NODE_ENV = process.env.ENV = 'production'
|
||||
const HOST = process.env.HOST || 'localhost'
|
||||
const PORT = process.env.PORT || 8080
|
||||
const METADATA = webpackMerge(commonConfig.metadata, {
|
||||
const METADATA = webpackMerge(commonConfig({env: ENV}).metadata, {
|
||||
host: HOST,
|
||||
port: PORT,
|
||||
ENV: ENV,
|
||||
HMR: false
|
||||
})
|
||||
|
||||
module.exports = webpackMerge(commonConfig, {
|
||||
module.exports = function (env) {
|
||||
return webpackMerge(commonConfig({env: ENV}), {
|
||||
/**
|
||||
* Switch loaders to debug mode.
|
||||
*
|
||||
@ -110,7 +113,7 @@ module.exports = webpackMerge(commonConfig, {
|
||||
* See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
|
||||
* See: https://github.com/webpack/docs/wiki/optimization#deduplication
|
||||
*/
|
||||
new DedupePlugin(),
|
||||
// new DedupePlugin(),
|
||||
|
||||
/**
|
||||
* Plugin: DefinePlugin
|
||||
@ -156,19 +159,16 @@ module.exports = webpackMerge(commonConfig, {
|
||||
// comments: true, //debug
|
||||
|
||||
beautify: false, // prod
|
||||
|
||||
mangle: {
|
||||
screw_ie8: true,
|
||||
keep_fnames: true
|
||||
}, // prod
|
||||
|
||||
compress: {
|
||||
screw_ie8: true
|
||||
}, // prod
|
||||
|
||||
mangle: { screw_ie8: true, keep_fnames: true }, // prod
|
||||
compress: { screw_ie8: true }, // prod
|
||||
comments: false // prod
|
||||
}),
|
||||
|
||||
new NormalModuleReplacementPlugin(
|
||||
/angular2-hmr/,
|
||||
helpers.root('config/modules/angular2-hmr-prod.js')
|
||||
)
|
||||
|
||||
/**
|
||||
* Plugin: CompressionPlugin
|
||||
* Description: Prepares compressed versions of assets to serve
|
||||
@ -176,10 +176,10 @@ module.exports = webpackMerge(commonConfig, {
|
||||
*
|
||||
* See: https://github.com/webpack/compression-webpack-plugin
|
||||
*/
|
||||
new CompressionPlugin({
|
||||
regExp: /\.css$|\.html$|\.js$|\.map$/,
|
||||
threshold: 2 * 1024
|
||||
})
|
||||
// new CompressionPlugin({
|
||||
// regExp: /\.css$|\.html$|\.js$|\.map$/,
|
||||
// threshold: 2 * 1024
|
||||
// })
|
||||
|
||||
],
|
||||
|
||||
@ -229,3 +229,4 @@ module.exports = webpackMerge(commonConfig, {
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
@ -13,61 +13,72 @@
|
||||
"url": "git://github.com/Chocobozzz/PeerTube.git"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "typings install",
|
||||
"test": "standard && tslint -c ./tslint.json src/**/*.ts",
|
||||
"webpack": "webpack"
|
||||
},
|
||||
"license": "GPLv3",
|
||||
"dependencies": {
|
||||
"@angular/common": "2.0.0-rc.4",
|
||||
"@angular/compiler": "2.0.0-rc.4",
|
||||
"@angular/core": "2.0.0-rc.4",
|
||||
"@angular/http": "2.0.0-rc.4",
|
||||
"@angular/platform-browser": "2.0.0-rc.4",
|
||||
"@angular/platform-browser-dynamic": "2.0.0-rc.4",
|
||||
"@angular/router": "3.0.0-beta.2",
|
||||
"angular-pipes": "^2.0.0",
|
||||
"awesome-typescript-loader": "^0.17.0",
|
||||
"bootstrap-loader": "^1.0.8",
|
||||
"@angular/common": "^2.0.0",
|
||||
"@angular/compiler": "^2.0.0",
|
||||
"@angular/core": "^2.0.0",
|
||||
"@angular/forms": "^2.0.0",
|
||||
"@angular/http": "^2.0.0",
|
||||
"@angular/platform-browser": "^2.0.0",
|
||||
"@angular/platform-browser-dynamic": "^2.0.0",
|
||||
"@angular/router": "^3.0.0",
|
||||
"@angularclass/hmr": "^1.2.0",
|
||||
"@angularclass/hmr-loader": "^3.0.2",
|
||||
"@types/core-js": "^0.9.28",
|
||||
"@types/node": "^6.0.38",
|
||||
"@types/source-map": "^0.1.26",
|
||||
"@types/uglify-js": "^2.0.27",
|
||||
"@types/webpack": "^1.12.29",
|
||||
"angular-pipes": "^3.0.0",
|
||||
"angular2-template-loader": "^0.5.0",
|
||||
"assets-webpack-plugin": "^3.4.0",
|
||||
"awesome-typescript-loader": "^2.2.1",
|
||||
"bootstrap-loader": "^2.0.0-beta.11",
|
||||
"bootstrap-sass": "^3.3.6",
|
||||
"compression-webpack-plugin": "^0.3.1",
|
||||
"copy-webpack-plugin": "^3.0.1",
|
||||
"core-js": "^2.4.0",
|
||||
"css-loader": "^0.23.1",
|
||||
"core-js": "^2.4.1",
|
||||
"css-loader": "^0.25.0",
|
||||
"css-to-string-loader": "https://github.com/Chocobozzz/css-to-string-loader#patch-1",
|
||||
"es6-promise": "^3.0.2",
|
||||
"es6-promise-loader": "^1.0.1",
|
||||
"es6-shim": "^0.35.0",
|
||||
"file-loader": "^0.8.5",
|
||||
"extract-text-webpack-plugin": "^2.0.0-beta.4",
|
||||
"file-loader": "^0.9.0",
|
||||
"html-webpack-plugin": "^2.19.0",
|
||||
"ie-shim": "^0.1.0",
|
||||
"intl": "^1.2.4",
|
||||
"json-loader": "^0.5.4",
|
||||
"ng2-bootstrap": "1.0.16",
|
||||
"ng2-bootstrap": "^1.1.5",
|
||||
"ng2-file-upload": "^1.0.3",
|
||||
"node-sass": "^3.7.0",
|
||||
"node-sass": "^3.10.0",
|
||||
"normalize.css": "^4.1.1",
|
||||
"raw-loader": "^0.5.1",
|
||||
"reflect-metadata": "0.1.3",
|
||||
"resolve-url-loader": "^1.4.3",
|
||||
"rxjs": "5.0.0-beta.6",
|
||||
"sass-loader": "^3.2.0",
|
||||
"resolve-url-loader": "^1.6.0",
|
||||
"rxjs": "5.0.0-beta.12",
|
||||
"sass-loader": "^4.0.2",
|
||||
"source-map-loader": "^0.1.5",
|
||||
"string-replace-loader": "^1.0.3",
|
||||
"style-loader": "^0.13.1",
|
||||
"ts-helpers": "^1.1.1",
|
||||
"tslint": "^3.7.4",
|
||||
"tslint": "3.15.1",
|
||||
"tslint-loader": "^2.1.4",
|
||||
"typescript": "^1.8.10",
|
||||
"typings": "^1.0.4",
|
||||
"typescript": "^2.0.0",
|
||||
"url-loader": "^0.5.7",
|
||||
"webpack": "^1.13.1",
|
||||
"webpack": "2.1.0-beta.22",
|
||||
"webpack-md5-hash": "0.0.5",
|
||||
"webpack-merge": "^0.13.0",
|
||||
"webpack-merge": "^0.14.1",
|
||||
"webpack-notifier": "^1.3.0",
|
||||
"webtorrent": "^0.95.2",
|
||||
"zone.js": "0.6.12"
|
||||
"webtorrent": "^0.96.0",
|
||||
"zone.js": "0.6.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
"codelyzer": "0.0.19",
|
||||
"standard": "^7.0.1"
|
||||
"codelyzer": "0.0.28",
|
||||
"standard": "^8.0.0"
|
||||
}
|
||||
}
|
||||
|
27
client/src/app/account/account.component.html
Normal file
27
client/src/app/account/account.component.html
Normal file
@ -0,0 +1,27 @@
|
||||
<h3>Account</h3>
|
||||
|
||||
<div *ngIf="information" class="alert alert-success">{{ information }}</div>
|
||||
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
|
||||
|
||||
<form role="form" (ngSubmit)="changePassword()" [formGroup]="form">
|
||||
<div class="form-group">
|
||||
<label for="new-password">New password</label>
|
||||
<input
|
||||
type="password" class="form-control" id="new-password"
|
||||
formControlName="new-password"
|
||||
>
|
||||
<div *ngIf="formErrors['new-password']" class="alert alert-danger">
|
||||
{{ formErrors['new-password'] }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">Confirm new password</label>
|
||||
<input
|
||||
type="password" class="form-control" id="new-confirmed-password"
|
||||
formControlName="new-confirmed-password"
|
||||
>
|
||||
</div>
|
||||
|
||||
<input type="submit" value="Change password" class="btn btn-default" [disabled]="!form.valid">
|
||||
</form>
|
67
client/src/app/account/account.component.ts
Normal file
67
client/src/app/account/account.component.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { } from '@angular/common';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { AccountService } from './account.service';
|
||||
import { FormReactive, USER_PASSWORD } from '../shared';
|
||||
|
||||
@Component({
|
||||
selector: 'my-account',
|
||||
templateUrl: './account.component.html'
|
||||
})
|
||||
|
||||
export class AccountComponent extends FormReactive implements OnInit {
|
||||
information: string = null;
|
||||
error: string = null;
|
||||
|
||||
form: FormGroup;
|
||||
formErrors = {
|
||||
'new-password': '',
|
||||
'new-confirmed-password': ''
|
||||
};
|
||||
validationMessages = {
|
||||
'new-password': USER_PASSWORD.MESSAGES,
|
||||
'new-confirmed-password': USER_PASSWORD.MESSAGES
|
||||
};
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private formBuilder: FormBuilder,
|
||||
private router: Router
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
buildForm() {
|
||||
this.form = this.formBuilder.group({
|
||||
'new-password': [ '', USER_PASSWORD.VALIDATORS ],
|
||||
'new-confirmed-password': [ '', USER_PASSWORD.VALIDATORS ],
|
||||
});
|
||||
|
||||
this.form.valueChanges.subscribe(data => this.onValueChanged(data));
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.buildForm();
|
||||
}
|
||||
|
||||
changePassword() {
|
||||
const newPassword = this.form.value['new-password'];
|
||||
const newConfirmedPassword = this.form.value['new-confirmed-password'];
|
||||
|
||||
this.information = null;
|
||||
this.error = null;
|
||||
|
||||
if (newPassword !== newConfirmedPassword) {
|
||||
this.error = 'The new password and the confirmed password do not correspond.';
|
||||
return;
|
||||
}
|
||||
|
||||
this.accountService.changePassword(newPassword).subscribe(
|
||||
ok => this.information = 'Password updated.',
|
||||
|
||||
err => this.error = err
|
||||
);
|
||||
}
|
||||
}
|
5
client/src/app/account/account.routes.ts
Normal file
5
client/src/app/account/account.routes.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { AccountComponent } from './account.component';
|
||||
|
||||
export const AccountRoutes = [
|
||||
{ path: 'account', component: AccountComponent }
|
||||
];
|
25
client/src/app/account/account.service.ts
Normal file
25
client/src/app/account/account.service.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { AuthHttp, AuthService, RestExtractor } from '../shared';
|
||||
|
||||
@Injectable()
|
||||
export class AccountService {
|
||||
private static BASE_USERS_URL = '/api/v1/users/';
|
||||
|
||||
constructor(
|
||||
private authHttp: AuthHttp,
|
||||
private authService: AuthService,
|
||||
private restExtractor: RestExtractor
|
||||
) {}
|
||||
|
||||
changePassword(newPassword: string) {
|
||||
const url = AccountService.BASE_USERS_URL + this.authService.getUser().id;
|
||||
const body = {
|
||||
password: newPassword
|
||||
};
|
||||
|
||||
return this.authHttp.put(url, body)
|
||||
.map(this.restExtractor.extractDataBool)
|
||||
.catch((res) => this.restExtractor.handleError(res));
|
||||
}
|
||||
}
|
3
client/src/app/account/index.ts
Normal file
3
client/src/app/account/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './account.component';
|
||||
export * from './account.routes';
|
||||
export * from './account.service';
|
8
client/src/app/admin/admin.component.ts
Normal file
8
client/src/app/admin/admin.component.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: '<router-outlet></router-outlet>'
|
||||
})
|
||||
|
||||
export class AdminComponent {
|
||||
}
|
23
client/src/app/admin/admin.routes.ts
Normal file
23
client/src/app/admin/admin.routes.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { AdminComponent } from './admin.component';
|
||||
import { FriendsRoutes } from './friends';
|
||||
import { RequestsRoutes } from './requests';
|
||||
import { UsersRoutes } from './users';
|
||||
|
||||
export const AdminRoutes: Routes = [
|
||||
{
|
||||
path: 'admin',
|
||||
component: AdminComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'users',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
...FriendsRoutes,
|
||||
...RequestsRoutes,
|
||||
...UsersRoutes
|
||||
]
|
||||
}
|
||||
];
|
@ -0,0 +1,26 @@
|
||||
<h3>Make friends</h3>
|
||||
|
||||
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
|
||||
|
||||
<form (ngSubmit)="makeFriends()" [formGroup]="form">
|
||||
<div class="form-group" *ngFor="let url of urls; let id = index; trackBy:customTrackBy">
|
||||
<label for="username">Url</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="text" class="form-control" placeholder="http://domain.com"
|
||||
[id]="'url-' + id" [formControlName]="'url-' + id"
|
||||
/>
|
||||
<span class="input-group-btn">
|
||||
<button *ngIf="displayAddField(id)" (click)="addField()" class="btn btn-default" type="button">+</button>
|
||||
<button *ngIf="displayRemoveField(id)" (click)="removeField(id)" class="btn btn-default" type="button">-</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div [hidden]="form.controls['url-' + id].valid || form.controls['url-' + id].pristine" class="alert alert-warning">
|
||||
It should be a valid url.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="submit" value="Make friends" class="btn btn-default" [disabled]="!isFormValid()">
|
||||
</form>
|
@ -0,0 +1,7 @@
|
||||
table {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.input-group-btn button {
|
||||
width: 35px;
|
||||
}
|
108
client/src/app/admin/friends/friend-add/friend-add.component.ts
Normal file
108
client/src/app/admin/friends/friend-add/friend-add.component.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { validateUrl } from '../../../shared';
|
||||
import { FriendService } from '../shared';
|
||||
|
||||
@Component({
|
||||
selector: 'my-friend-add',
|
||||
templateUrl: './friend-add.component.html',
|
||||
styleUrls: [ './friend-add.component.scss' ]
|
||||
})
|
||||
export class FriendAddComponent implements OnInit {
|
||||
form: FormGroup;
|
||||
urls = [ ];
|
||||
error: string = null;
|
||||
|
||||
constructor(private router: Router, private friendService: FriendService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.form = new FormGroup({});
|
||||
this.addField();
|
||||
}
|
||||
|
||||
addField() {
|
||||
this.form.addControl(`url-${this.urls.length}`, new FormControl('', [ validateUrl ]));
|
||||
this.urls.push('');
|
||||
}
|
||||
|
||||
customTrackBy(index: number, obj: any): any {
|
||||
return index;
|
||||
}
|
||||
|
||||
displayAddField(index: number) {
|
||||
return index === (this.urls.length - 1);
|
||||
}
|
||||
|
||||
displayRemoveField(index: number) {
|
||||
return (index !== 0 || this.urls.length > 1) && index !== (this.urls.length - 1);
|
||||
}
|
||||
|
||||
isFormValid() {
|
||||
// Do not check the last input
|
||||
for (let i = 0; i < this.urls.length - 1; i++) {
|
||||
if (!this.form.controls[`url-${i}`].valid) return false;
|
||||
}
|
||||
|
||||
const lastIndex = this.urls.length - 1;
|
||||
// If the last input (which is not the first) is empty, it's ok
|
||||
if (this.urls[lastIndex] === '' && lastIndex !== 0) {
|
||||
return true;
|
||||
} else {
|
||||
return this.form.controls[`url-${lastIndex}`].valid;
|
||||
}
|
||||
}
|
||||
|
||||
removeField(index: number) {
|
||||
// Remove the last control
|
||||
this.form.removeControl(`url-${this.urls.length - 1}`);
|
||||
this.urls.splice(index, 1);
|
||||
}
|
||||
|
||||
makeFriends() {
|
||||
this.error = '';
|
||||
|
||||
const notEmptyUrls = this.getNotEmptyUrls();
|
||||
if (notEmptyUrls.length === 0) {
|
||||
this.error = 'You need to specify at less 1 url.';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isUrlsUnique(notEmptyUrls)) {
|
||||
this.error = 'Urls need to be unique.';
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmMessage = 'Are you sure to make friends with:\n - ' + notEmptyUrls.join('\n - ');
|
||||
if (!confirm(confirmMessage)) return;
|
||||
|
||||
this.friendService.makeFriends(notEmptyUrls).subscribe(
|
||||
status => {
|
||||
// TODO: extractdatastatus
|
||||
// if (status === 409) {
|
||||
// alert('Already made friends!');
|
||||
// } else {
|
||||
alert('Make friends request sent!');
|
||||
this.router.navigate([ '/admin/friends/list' ]);
|
||||
// }
|
||||
},
|
||||
error => alert(error.text)
|
||||
);
|
||||
}
|
||||
|
||||
private getNotEmptyUrls() {
|
||||
const notEmptyUrls = [];
|
||||
|
||||
Object.keys(this.form.value).forEach((urlKey) => {
|
||||
const url = this.form.value[urlKey];
|
||||
if (url !== '') notEmptyUrls.push(url);
|
||||
});
|
||||
|
||||
return notEmptyUrls;
|
||||
}
|
||||
|
||||
private isUrlsUnique(urls: string[]) {
|
||||
return urls.every(url => urls.indexOf(url) === urls.lastIndexOf(url));
|
||||
}
|
||||
}
|
1
client/src/app/admin/friends/friend-add/index.ts
Normal file
1
client/src/app/admin/friends/friend-add/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './friend-add.component';
|
@ -0,0 +1,29 @@
|
||||
<h3>Friends list</h3>
|
||||
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="table-column-id">ID</th>
|
||||
<th>Url</th>
|
||||
<th>Score</th>
|
||||
<th>Created Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr *ngFor="let friend of friends">
|
||||
<td>{{ friend.id }}</td>
|
||||
<td>{{ friend.url }}</td>
|
||||
<td>{{ friend.score }}</td>
|
||||
<td>{{ friend.createdDate | date: 'medium' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<a *ngIf="friends?.length !== 0" class="add-user btn btn-danger pull-left" (click)="quitFriends()">
|
||||
Quit friends
|
||||
</a>
|
||||
|
||||
<a *ngIf="friends?.length === 0" class="add-user btn btn-success pull-right" [routerLink]="['/admin/friends/add']">
|
||||
Make friends
|
||||
</a>
|
@ -0,0 +1,3 @@
|
||||
table {
|
||||
margin-bottom: 40px;
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
import { Friend, FriendService } from '../shared';
|
||||
|
||||
@Component({
|
||||
selector: 'my-friend-list',
|
||||
templateUrl: './friend-list.component.html',
|
||||
styleUrls: [ './friend-list.component.scss' ]
|
||||
})
|
||||
export class FriendListComponent implements OnInit {
|
||||
friends: Friend[];
|
||||
|
||||
constructor(private friendService: FriendService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.getFriends();
|
||||
}
|
||||
|
||||
quitFriends() {
|
||||
if (!confirm('Are you sure?')) return;
|
||||
|
||||
this.friendService.quitFriends().subscribe(
|
||||
status => {
|
||||
alert('Quit friends!');
|
||||
this.getFriends();
|
||||
},
|
||||
error => alert(error.text)
|
||||
);
|
||||
}
|
||||
|
||||
private getFriends() {
|
||||
this.friendService.getFriends().subscribe(
|
||||
friends => this.friends = friends,
|
||||
|
||||
err => alert(err.text)
|
||||
);
|
||||
}
|
||||
}
|
1
client/src/app/admin/friends/friend-list/index.ts
Normal file
1
client/src/app/admin/friends/friend-list/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './friend-list.component';
|
8
client/src/app/admin/friends/friends.component.ts
Normal file
8
client/src/app/admin/friends/friends.component.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: '<router-outlet></router-outlet>'
|
||||
})
|
||||
|
||||
export class FriendsComponent {
|
||||
}
|
27
client/src/app/admin/friends/friends.routes.ts
Normal file
27
client/src/app/admin/friends/friends.routes.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { FriendsComponent } from './friends.component';
|
||||
import { FriendAddComponent } from './friend-add';
|
||||
import { FriendListComponent } from './friend-list';
|
||||
|
||||
export const FriendsRoutes: Routes = [
|
||||
{
|
||||
path: 'friends',
|
||||
component: FriendsComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'list',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
component: FriendListComponent
|
||||
},
|
||||
{
|
||||
path: 'add',
|
||||
component: FriendAddComponent
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
5
client/src/app/admin/friends/index.ts
Normal file
5
client/src/app/admin/friends/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './friend-add';
|
||||
export * from './friend-list';
|
||||
export * from './shared';
|
||||
export * from './friends.component';
|
||||
export * from './friends.routes';
|
6
client/src/app/admin/friends/shared/friend.model.ts
Normal file
6
client/src/app/admin/friends/shared/friend.model.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface Friend {
|
||||
id: string;
|
||||
url: string;
|
||||
score: number;
|
||||
createdDate: Date;
|
||||
}
|
39
client/src/app/admin/friends/shared/friend.service.ts
Normal file
39
client/src/app/admin/friends/shared/friend.service.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { Friend } from './friend.model';
|
||||
import { AuthHttp, RestExtractor } from '../../../shared';
|
||||
|
||||
@Injectable()
|
||||
export class FriendService {
|
||||
private static BASE_FRIEND_URL: string = '/api/v1/pods/';
|
||||
|
||||
constructor (
|
||||
private authHttp: AuthHttp,
|
||||
private restExtractor: RestExtractor
|
||||
) {}
|
||||
|
||||
getFriends(): Observable<Friend[]> {
|
||||
return this.authHttp.get(FriendService.BASE_FRIEND_URL)
|
||||
// Not implemented as a data list by the server yet
|
||||
// .map(this.restExtractor.extractDataList)
|
||||
.map((res) => res.json())
|
||||
.catch((res) => this.restExtractor.handleError(res));
|
||||
}
|
||||
|
||||
makeFriends(notEmptyUrls) {
|
||||
const body = {
|
||||
urls: notEmptyUrls
|
||||
};
|
||||
|
||||
return this.authHttp.post(FriendService.BASE_FRIEND_URL + 'makefriends', body)
|
||||
.map(this.restExtractor.extractDataBool)
|
||||
.catch((res) => this.restExtractor.handleError(res));
|
||||
}
|
||||
|
||||
quitFriends() {
|
||||
return this.authHttp.get(FriendService.BASE_FRIEND_URL + 'quitfriends')
|
||||
.map(res => res.status)
|
||||
.catch((res) => this.restExtractor.handleError(res));
|
||||
}
|
||||
}
|
@ -1 +1,2 @@
|
||||
export * from './friend.model';
|
||||
export * from './friend.service';
|
6
client/src/app/admin/index.ts
Normal file
6
client/src/app/admin/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * from './friends';
|
||||
export * from './requests';
|
||||
export * from './users';
|
||||
export * from './admin.component';
|
||||
export * from './admin.routes';
|
||||
export * from './menu-admin.component';
|
26
client/src/app/admin/menu-admin.component.html
Normal file
26
client/src/app/admin/menu-admin.component.html
Normal file
@ -0,0 +1,26 @@
|
||||
<menu class="col-md-2 col-sm-3 col-xs-3">
|
||||
|
||||
<div class="panel-block">
|
||||
<div id="panel-users" class="panel-button">
|
||||
<span class="hidden-xs glyphicon glyphicon-user"></span>
|
||||
<a [routerLink]="['/admin/users/list']">List users</a>
|
||||
</div>
|
||||
|
||||
<div id="panel-friends" class="panel-button">
|
||||
<span class="hidden-xs glyphicon glyphicon-cloud"></span>
|
||||
<a [routerLink]="['/admin/friends/list']">List friends</a>
|
||||
</div>
|
||||
|
||||
<div id="panel-request-stats" class="panel-button">
|
||||
<span class="hidden-xs glyphicon glyphicon-stats"></span>
|
||||
<a [routerLink]="['/admin/requests/stats']">Request stats</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-block">
|
||||
<div id="panel-quit-administration" class="panel-button">
|
||||
<span class="hidden-xs glyphicon glyphicon-cog"></span>
|
||||
<a [routerLink]="['/videos/list']">Quit admin.</a>
|
||||
</div>
|
||||
</div>
|
||||
</menu>
|
7
client/src/app/admin/menu-admin.component.ts
Normal file
7
client/src/app/admin/menu-admin.component.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'my-menu-admin',
|
||||
templateUrl: './menu-admin.component.html'
|
||||
})
|
||||
export class MenuAdminComponent { }
|
4
client/src/app/admin/requests/index.ts
Normal file
4
client/src/app/admin/requests/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './request-stats';
|
||||
export * from './shared';
|
||||
export * from './requests.component';
|
||||
export * from './requests.routes';
|
1
client/src/app/admin/requests/request-stats/index.ts
Normal file
1
client/src/app/admin/requests/request-stats/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './request-stats.component';
|
@ -0,0 +1,23 @@
|
||||
<h3>Requests stats</h3>
|
||||
|
||||
<div *ngIf="stats !== null">
|
||||
<div>
|
||||
<span class="label-description">Interval seconds between requests:</span>
|
||||
{{ stats.secondsInterval }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="label-description">Remaining time before the scheduled request:</span>
|
||||
{{ stats.remainingSeconds }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="label-description">Maximum number of requests per interval:</span>
|
||||
{{ stats.maxRequestsInParallel }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="label-description">Remaining requests:</span>
|
||||
{{ stats.requests.length }}
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,6 @@
|
||||
.label-description {
|
||||
display: inline-block;
|
||||
width: 350px;
|
||||
font-weight: bold;
|
||||
color: black;
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
|
||||
import { RequestService, RequestStats } from '../shared';
|
||||
|
||||
@Component({
|
||||
selector: 'my-request-stats',
|
||||
templateUrl: './request-stats.component.html',
|
||||
styleUrls: [ './request-stats.component.scss' ]
|
||||
})
|
||||
export class RequestStatsComponent implements OnInit, OnDestroy {
|
||||
stats: RequestStats = null;
|
||||
|
||||
private interval: NodeJS.Timer = null;
|
||||
|
||||
constructor(private requestService: RequestService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.getStats();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.stats.secondsInterval !== null) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
}
|
||||
|
||||
getStats() {
|
||||
this.requestService.getStats().subscribe(
|
||||
stats => {
|
||||
console.log(stats);
|
||||
this.stats = stats;
|
||||
this.runInterval();
|
||||
},
|
||||
|
||||
err => alert(err.text)
|
||||
);
|
||||
}
|
||||
|
||||
private runInterval() {
|
||||
this.interval = setInterval(() => {
|
||||
this.stats.remainingMilliSeconds -= 1000;
|
||||
|
||||
if (this.stats.remainingMilliSeconds <= 0) {
|
||||
setTimeout(() => this.getStats(), this.stats.remainingMilliSeconds + 100);
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
|
||||
}
|
8
client/src/app/admin/requests/requests.component.ts
Normal file
8
client/src/app/admin/requests/requests.component.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: '<router-outlet></router-outlet>'
|
||||
})
|
||||
|
||||
export class RequestsComponent {
|
||||
}
|
22
client/src/app/admin/requests/requests.routes.ts
Normal file
22
client/src/app/admin/requests/requests.routes.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { RequestsComponent } from './requests.component';
|
||||
import { RequestStatsComponent } from './request-stats';
|
||||
|
||||
export const RequestsRoutes: Routes = [
|
||||
{
|
||||
path: 'requests',
|
||||
component: RequestsComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'stats',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'stats',
|
||||
component: RequestStatsComponent
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
2
client/src/app/admin/requests/shared/index.ts
Normal file
2
client/src/app/admin/requests/shared/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './request-stats.model';
|
||||
export * from './request.service';
|
32
client/src/app/admin/requests/shared/request-stats.model.ts
Normal file
32
client/src/app/admin/requests/shared/request-stats.model.ts
Normal file
@ -0,0 +1,32 @@
|
||||
export interface Request {
|
||||
request: any;
|
||||
to: any;
|
||||
}
|
||||
|
||||
export class RequestStats {
|
||||
maxRequestsInParallel: number;
|
||||
milliSecondsInterval: number;
|
||||
remainingMilliSeconds: number;
|
||||
requests: Request[];
|
||||
|
||||
constructor(hash: {
|
||||
maxRequestsInParallel: number,
|
||||
milliSecondsInterval: number,
|
||||
remainingMilliSeconds: number,
|
||||
requests: Request[];
|
||||
}) {
|
||||
this.maxRequestsInParallel = hash.maxRequestsInParallel;
|
||||
this.milliSecondsInterval = hash.milliSecondsInterval;
|
||||
this.remainingMilliSeconds = hash.remainingMilliSeconds;
|
||||
this.requests = hash.requests;
|
||||
}
|
||||
|
||||
get remainingSeconds() {
|
||||
return Math.floor(this.remainingMilliSeconds / 1000);
|
||||
}
|
||||
|
||||
get secondsInterval() {
|
||||
return Math.floor(this.milliSecondsInterval / 1000);
|
||||
}
|
||||
|
||||
}
|
22
client/src/app/admin/requests/shared/request.service.ts
Normal file
22
client/src/app/admin/requests/shared/request.service.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { RequestStats } from './request-stats.model';
|
||||
import { AuthHttp, RestExtractor } from '../../../shared';
|
||||
|
||||
@Injectable()
|
||||
export class RequestService {
|
||||
private static BASE_REQUEST_URL: string = '/api/v1/requests/';
|
||||
|
||||
constructor (
|
||||
private authHttp: AuthHttp,
|
||||
private restExtractor: RestExtractor
|
||||
) {}
|
||||
|
||||
getStats(): Observable<RequestStats> {
|
||||
return this.authHttp.get(RequestService.BASE_REQUEST_URL + 'stats')
|
||||
.map(this.restExtractor.extractDataGet)
|
||||
.map((data) => new RequestStats(data))
|
||||
.catch((res) => this.restExtractor.handleError(res));
|
||||
}
|
||||
}
|
5
client/src/app/admin/users/index.ts
Normal file
5
client/src/app/admin/users/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './shared';
|
||||
export * from './user-add';
|
||||
export * from './user-list';
|
||||
export * from './users.component';
|
||||
export * from './users.routes';
|
1
client/src/app/admin/users/shared/index.ts
Normal file
1
client/src/app/admin/users/shared/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './user.service';
|
47
client/src/app/admin/users/shared/user.service.ts
Normal file
47
client/src/app/admin/users/shared/user.service.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { AuthHttp, RestExtractor, ResultList, User } from '../../../shared';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
// TODO: merge this constant with account
|
||||
private static BASE_USERS_URL = '/api/v1/users/';
|
||||
|
||||
constructor(
|
||||
private authHttp: AuthHttp,
|
||||
private restExtractor: RestExtractor
|
||||
) {}
|
||||
|
||||
addUser(username: string, password: string) {
|
||||
const body = {
|
||||
username,
|
||||
password
|
||||
};
|
||||
|
||||
return this.authHttp.post(UserService.BASE_USERS_URL, body)
|
||||
.map(this.restExtractor.extractDataBool)
|
||||
.catch(this.restExtractor.handleError);
|
||||
}
|
||||
|
||||
getUsers() {
|
||||
return this.authHttp.get(UserService.BASE_USERS_URL)
|
||||
.map(this.restExtractor.extractDataList)
|
||||
.map(this.extractUsers)
|
||||
.catch((res) => this.restExtractor.handleError(res));
|
||||
}
|
||||
|
||||
removeUser(user: User) {
|
||||
return this.authHttp.delete(UserService.BASE_USERS_URL + user.id);
|
||||
}
|
||||
|
||||
private extractUsers(result: ResultList) {
|
||||
const usersJson = result.data;
|
||||
const totalUsers = result.total;
|
||||
const users = [];
|
||||
for (const userJson of usersJson) {
|
||||
users.push(new User(userJson));
|
||||
}
|
||||
|
||||
return { users, totalUsers };
|
||||
}
|
||||
}
|
1
client/src/app/admin/users/user-add/index.ts
Normal file
1
client/src/app/admin/users/user-add/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './user-add.component';
|
29
client/src/app/admin/users/user-add/user-add.component.html
Normal file
29
client/src/app/admin/users/user-add/user-add.component.html
Normal file
@ -0,0 +1,29 @@
|
||||
<h3>Add user</h3>
|
||||
|
||||
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
|
||||
|
||||
<form role="form" (ngSubmit)="addUser()" [formGroup]="form">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input
|
||||
type="text" class="form-control" id="username" placeholder="Username"
|
||||
formControlName="username"
|
||||
>
|
||||
<div *ngIf="formErrors.username" class="alert alert-danger">
|
||||
{{ formErrors.username }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input
|
||||
type="password" class="form-control" id="password" placeholder="Password"
|
||||
formControlName="password"
|
||||
>
|
||||
<div *ngIf="formErrors.password" class="alert alert-danger">
|
||||
{{ formErrors.password }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="submit" value="Add user" class="btn btn-default" [disabled]="!form.valid">
|
||||
</form>
|
57
client/src/app/admin/users/user-add/user-add.component.ts
Normal file
57
client/src/app/admin/users/user-add/user-add.component.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { UserService } from '../shared';
|
||||
import { FormReactive, USER_USERNAME, USER_PASSWORD } from '../../../shared';
|
||||
|
||||
@Component({
|
||||
selector: 'my-user-add',
|
||||
templateUrl: './user-add.component.html'
|
||||
})
|
||||
export class UserAddComponent extends FormReactive implements OnInit {
|
||||
error: string = null;
|
||||
|
||||
form: FormGroup;
|
||||
formErrors = {
|
||||
'username': '',
|
||||
'password': ''
|
||||
};
|
||||
validationMessages = {
|
||||
'username': USER_USERNAME.MESSAGES,
|
||||
'password': USER_PASSWORD.MESSAGES,
|
||||
};
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private router: Router,
|
||||
private userService: UserService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
buildForm() {
|
||||
this.form = this.formBuilder.group({
|
||||
username: [ '', USER_USERNAME.VALIDATORS ],
|
||||
password: [ '', USER_PASSWORD.VALIDATORS ],
|
||||
});
|
||||
|
||||
this.form.valueChanges.subscribe(data => this.onValueChanged(data));
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.buildForm();
|
||||
}
|
||||
|
||||
addUser() {
|
||||
this.error = null;
|
||||
|
||||
const { username, password } = this.form.value;
|
||||
|
||||
this.userService.addUser(username, password).subscribe(
|
||||
ok => this.router.navigate([ '/admin/users/list' ]),
|
||||
|
||||
err => this.error = err.text
|
||||
);
|
||||
}
|
||||
}
|
1
client/src/app/admin/users/user-list/index.ts
Normal file
1
client/src/app/admin/users/user-list/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './user-list.component';
|
@ -0,0 +1,28 @@
|
||||
<h3>Users list</h3>
|
||||
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="table-column-id">ID</th>
|
||||
<th>Username</th>
|
||||
<th>Created Date</th>
|
||||
<th class="text-right">Remove</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr *ngFor="let user of users">
|
||||
<td>{{ user.id }}</td>
|
||||
<td>{{ user.username }}</td>
|
||||
<td>{{ user.createdDate | date: 'medium' }}</td>
|
||||
<td class="text-right">
|
||||
<span class="glyphicon glyphicon-remove" *ngIf="!user.isAdmin()" (click)="removeUser(user)"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<a class="add-user btn btn-success pull-right" [routerLink]="['/admin/users/add']">
|
||||
<span class="glyphicon glyphicon-plus"></span>
|
||||
Add user
|
||||
</a>
|
@ -0,0 +1,7 @@
|
||||
.glyphicon-remove {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.add-user {
|
||||
margin-top: 10px;
|
||||
}
|
42
client/src/app/admin/users/user-list/user-list.component.ts
Normal file
42
client/src/app/admin/users/user-list/user-list.component.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
import { User } from '../../../shared';
|
||||
import { UserService } from '../shared';
|
||||
|
||||
@Component({
|
||||
selector: 'my-user-list',
|
||||
templateUrl: './user-list.component.html',
|
||||
styleUrls: [ './user-list.component.scss' ]
|
||||
})
|
||||
export class UserListComponent implements OnInit {
|
||||
totalUsers: number;
|
||||
users: User[];
|
||||
|
||||
constructor(private userService: UserService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.getUsers();
|
||||
}
|
||||
|
||||
getUsers() {
|
||||
this.userService.getUsers().subscribe(
|
||||
({ users, totalUsers }) => {
|
||||
this.users = users;
|
||||
this.totalUsers = totalUsers;
|
||||
},
|
||||
|
||||
err => alert(err.text)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
removeUser(user: User) {
|
||||
if (confirm('Are you sure?')) {
|
||||
this.userService.removeUser(user).subscribe(
|
||||
() => this.getUsers(),
|
||||
|
||||
err => alert(err.text)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
8
client/src/app/admin/users/users.component.ts
Normal file
8
client/src/app/admin/users/users.component.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: '<router-outlet></router-outlet>'
|
||||
})
|
||||
|
||||
export class UsersComponent {
|
||||
}
|
27
client/src/app/admin/users/users.routes.ts
Normal file
27
client/src/app/admin/users/users.routes.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { UsersComponent } from './users.component';
|
||||
import { UserAddComponent } from './user-add';
|
||||
import { UserListComponent } from './user-list';
|
||||
|
||||
export const UsersRoutes: Routes = [
|
||||
{
|
||||
path: 'users',
|
||||
component: UsersComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'list',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
component: UserListComponent
|
||||
},
|
||||
{
|
||||
path: 'add',
|
||||
component: UserAddComponent
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
@ -14,48 +14,14 @@
|
||||
|
||||
|
||||
<div class="row">
|
||||
|
||||
<menu class="col-md-2 col-sm-3 col-xs-3">
|
||||
<div class="panel-block">
|
||||
<div id="panel-user-login" class="panel-button">
|
||||
<span class="hidden-xs glyphicon glyphicon-user"></span>
|
||||
<a *ngIf="!isLoggedIn" [routerLink]="['/login']">Login</a>
|
||||
<a *ngIf="isLoggedIn" (click)="logout()">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-block">
|
||||
<div id="panel-get-videos" class="panel-button">
|
||||
<span class="hidden-xs glyphicon glyphicon-list"></span>
|
||||
<a [routerLink]="['/videos/list']">Get videos</a>
|
||||
</div>
|
||||
|
||||
<div id="panel-upload-video" class="panel-button" *ngIf="isLoggedIn">
|
||||
<span class="hidden-xs glyphicon glyphicon-cloud-upload"></span>
|
||||
<a [routerLink]="['/videos/add']">Upload a video</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-block" *ngIf="isLoggedIn">
|
||||
<div id="panel-make-friends" class="panel-button">
|
||||
<span class="hidden-xs glyphicon glyphicon-cloud"></span>
|
||||
<a (click)='makeFriends()'>Make friends</a>
|
||||
</div>
|
||||
|
||||
<div id="panel-quit-friends" class="panel-button">
|
||||
<span class="hidden-xs glyphicon glyphicon-plane"></span>
|
||||
<a (click)='quitFriends()'>Quit friends</a>
|
||||
</div>
|
||||
</div>
|
||||
</menu>
|
||||
<my-menu *ngIf="isInAdmin() === false"></my-menu>
|
||||
<my-menu-admin *ngIf="isInAdmin() === true"></my-menu-admin>
|
||||
|
||||
<div class="col-md-9 col-sm-8 col-xs-8 router-outlet-container">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<footer>
|
||||
PeerTube, CopyLeft 2015-2016
|
||||
</footer>
|
||||
|
@ -12,40 +12,6 @@ header div {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
menu {
|
||||
@media screen and (max-width: 600px) {
|
||||
margin-right: 3px !important;
|
||||
padding: 3px !important;
|
||||
min-height: 400px !important;
|
||||
}
|
||||
|
||||
min-height: 600px;
|
||||
margin-right: 20px;
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.2);
|
||||
|
||||
.panel-button {
|
||||
margin: 8px;
|
||||
cursor: pointer;
|
||||
transition: margin 0.2s;
|
||||
|
||||
&:hover {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
|
||||
.glyphicon {
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-block:not(:last-child) {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.router-outlet-container {
|
||||
@media screen and (max-width: 400px) {
|
||||
padding: 0 3px 0 3px;
|
||||
|
@ -1,73 +1,16 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute, Router, ROUTER_DIRECTIVES } from '@angular/router';
|
||||
|
||||
import { FriendService } from './friends';
|
||||
import {
|
||||
AuthService,
|
||||
AuthStatus,
|
||||
SearchComponent,
|
||||
SearchService
|
||||
} from './shared';
|
||||
import { VideoService } from './videos';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'my-app',
|
||||
template: require('./app.component.html'),
|
||||
styles: [ require('./app.component.scss') ],
|
||||
directives: [ ROUTER_DIRECTIVES, SearchComponent ],
|
||||
providers: [ FriendService, VideoService, SearchService ]
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: [ './app.component.scss' ]
|
||||
})
|
||||
|
||||
export class AppComponent {
|
||||
choices = [];
|
||||
isLoggedIn: boolean;
|
||||
constructor(private router: Router) {}
|
||||
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private friendService: FriendService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
) {
|
||||
this.isLoggedIn = this.authService.isLoggedIn();
|
||||
|
||||
this.authService.loginChangedSource.subscribe(
|
||||
status => {
|
||||
if (status === AuthStatus.LoggedIn) {
|
||||
this.isLoggedIn = true;
|
||||
console.log('Logged in.');
|
||||
} else if (status === AuthStatus.LoggedOut) {
|
||||
this.isLoggedIn = false;
|
||||
console.log('Logged out.');
|
||||
} else {
|
||||
console.error('Unknown auth status: ' + status);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.authService.logout();
|
||||
}
|
||||
|
||||
makeFriends() {
|
||||
this.friendService.makeFriends().subscribe(
|
||||
status => {
|
||||
if (status === 409) {
|
||||
alert('Already made friends!');
|
||||
} else {
|
||||
alert('Made friends!');
|
||||
}
|
||||
},
|
||||
error => alert(error)
|
||||
);
|
||||
}
|
||||
|
||||
quitFriends() {
|
||||
this.friendService.quitFriends().subscribe(
|
||||
status => {
|
||||
alert('Quit friends!');
|
||||
},
|
||||
error => alert(error)
|
||||
);
|
||||
isInAdmin() {
|
||||
return this.router.url.indexOf('/admin/') !== -1;
|
||||
}
|
||||
}
|
||||
|
146
client/src/app/app.module.ts
Normal file
146
client/src/app/app.module.ts
Normal file
@ -0,0 +1,146 @@
|
||||
import { ApplicationRef, NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { HttpModule, RequestOptions, XHRBackend } from '@angular/http';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { removeNgStyles, createNewHosts } from '@angularclass/hmr';
|
||||
|
||||
import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe';
|
||||
import { ProgressbarModule } from 'ng2-bootstrap/components/progressbar';
|
||||
import { PaginationModule } from 'ng2-bootstrap/components/pagination';
|
||||
import { FileUploadModule } from 'ng2-file-upload/ng2-file-upload';
|
||||
|
||||
/*
|
||||
* Platform and Environment providers/directives/pipes
|
||||
*/
|
||||
import { ENV_PROVIDERS } from './environment';
|
||||
import { routes } from './app.routes';
|
||||
// App is our top level component
|
||||
import { AppComponent } from './app.component';
|
||||
import { AppState } from './app.service';
|
||||
|
||||
import {
|
||||
AdminComponent,
|
||||
FriendsComponent,
|
||||
FriendAddComponent,
|
||||
FriendListComponent,
|
||||
FriendService,
|
||||
MenuAdminComponent,
|
||||
RequestsComponent,
|
||||
RequestStatsComponent,
|
||||
RequestService,
|
||||
UsersComponent,
|
||||
UserAddComponent,
|
||||
UserListComponent,
|
||||
UserService
|
||||
} from './admin';
|
||||
import { AccountComponent, AccountService } from './account';
|
||||
import { LoginComponent } from './login';
|
||||
import { MenuComponent } from './menu.component';
|
||||
import { AuthService, AuthHttp, RestExtractor, RestService, SearchComponent, SearchService } from './shared';
|
||||
import {
|
||||
LoaderComponent,
|
||||
VideosComponent,
|
||||
VideoAddComponent,
|
||||
VideoListComponent,
|
||||
VideoMiniatureComponent,
|
||||
VideoSortComponent,
|
||||
VideoWatchComponent,
|
||||
VideoService,
|
||||
WebTorrentService
|
||||
} from './videos';
|
||||
|
||||
// Application wide providers
|
||||
const APP_PROVIDERS = [
|
||||
AppState,
|
||||
|
||||
{
|
||||
provide: AuthHttp,
|
||||
useFactory: (backend: XHRBackend, defaultOptions: RequestOptions, authService: AuthService) => {
|
||||
return new AuthHttp(backend, defaultOptions, authService);
|
||||
},
|
||||
deps: [ XHRBackend, RequestOptions, AuthService ]
|
||||
},
|
||||
|
||||
AuthService,
|
||||
RestExtractor,
|
||||
RestService,
|
||||
|
||||
VideoService,
|
||||
SearchService,
|
||||
FriendService,
|
||||
RequestService,
|
||||
UserService,
|
||||
AccountService,
|
||||
WebTorrentService
|
||||
];
|
||||
/**
|
||||
* `AppModule` is the main entry point into Angular2's bootstraping process
|
||||
*/
|
||||
@NgModule({
|
||||
bootstrap: [ AppComponent ],
|
||||
declarations: [
|
||||
AccountComponent,
|
||||
AdminComponent,
|
||||
AppComponent,
|
||||
BytesPipe,
|
||||
FriendAddComponent,
|
||||
FriendListComponent,
|
||||
FriendsComponent,
|
||||
LoaderComponent,
|
||||
LoginComponent,
|
||||
MenuAdminComponent,
|
||||
MenuComponent,
|
||||
RequestsComponent,
|
||||
RequestStatsComponent,
|
||||
SearchComponent,
|
||||
UserAddComponent,
|
||||
UserListComponent,
|
||||
UsersComponent,
|
||||
VideoAddComponent,
|
||||
VideoListComponent,
|
||||
VideoMiniatureComponent,
|
||||
VideosComponent,
|
||||
VideoSortComponent,
|
||||
VideoWatchComponent,
|
||||
],
|
||||
imports: [ // import Angular's modules
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
HttpModule,
|
||||
RouterModule.forRoot(routes),
|
||||
|
||||
ProgressbarModule,
|
||||
PaginationModule,
|
||||
FileUploadModule
|
||||
],
|
||||
providers: [ // expose our Services and Providers into Angular's dependency injection
|
||||
ENV_PROVIDERS,
|
||||
APP_PROVIDERS
|
||||
]
|
||||
})
|
||||
export class AppModule {
|
||||
constructor(public appRef: ApplicationRef, public appState: AppState) {}
|
||||
hmrOnInit(store) {
|
||||
if (!store || !store.state) return;
|
||||
console.log('HMR store', store);
|
||||
this.appState._state = store.state;
|
||||
this.appRef.tick();
|
||||
delete store.state;
|
||||
}
|
||||
hmrOnDestroy(store) {
|
||||
const cmpLocation = this.appRef.components.map(cmp => cmp.location.nativeElement);
|
||||
// recreate elements
|
||||
const state = this.appState._state;
|
||||
store.state = state;
|
||||
store.disposeOldHosts = createNewHosts(cmpLocation);
|
||||
// remove styles
|
||||
removeNgStyles();
|
||||
}
|
||||
hmrAfterDestroy(store) {
|
||||
// display new elements
|
||||
store.disposeOldHosts();
|
||||
delete store.disposeOldHosts;
|
||||
}
|
||||
}
|
@ -1,15 +1,18 @@
|
||||
import { RouterConfig } from '@angular/router';
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { AccountRoutes } from './account';
|
||||
import { LoginRoutes } from './login';
|
||||
import { AdminRoutes } from './admin';
|
||||
import { VideosRoutes } from './videos';
|
||||
|
||||
export const routes: RouterConfig = [
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: '/videos/list',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
|
||||
...AdminRoutes,
|
||||
...AccountRoutes,
|
||||
...LoginRoutes,
|
||||
...VideosRoutes
|
||||
];
|
||||
|
36
client/src/app/app.service.ts
Normal file
36
client/src/app/app.service.ts
Normal file
@ -0,0 +1,36 @@
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class AppState {
|
||||
_state = { };
|
||||
|
||||
constructor() { ; }
|
||||
|
||||
// already return a clone of the current state
|
||||
get state() {
|
||||
return this._state = this._clone(this._state);
|
||||
}
|
||||
// never allow mutation
|
||||
set state(value) {
|
||||
throw new Error('do not mutate the `.state` directly');
|
||||
}
|
||||
|
||||
|
||||
get(prop?: any) {
|
||||
// use our state getter for the clone
|
||||
const state = this.state;
|
||||
return state.hasOwnProperty(prop) ? state[prop] : state;
|
||||
}
|
||||
|
||||
set(prop: string, value: any) {
|
||||
// internally mutate our state
|
||||
return this._state[prop] = value;
|
||||
}
|
||||
|
||||
|
||||
_clone(object) {
|
||||
// simple object clone
|
||||
return JSON.parse(JSON.stringify( object ));
|
||||
}
|
||||
}
|
50
client/src/app/environment.ts
Normal file
50
client/src/app/environment.ts
Normal file
@ -0,0 +1,50 @@
|
||||
|
||||
// Angular 2
|
||||
// rc2 workaround
|
||||
import { enableDebugTools, disableDebugTools } from '@angular/platform-browser';
|
||||
import { enableProdMode, ApplicationRef } from '@angular/core';
|
||||
// Environment Providers
|
||||
let PROVIDERS = [
|
||||
// common env directives
|
||||
];
|
||||
|
||||
// Angular debug tools in the dev console
|
||||
// https://github.com/angular/angular/blob/86405345b781a9dc2438c0fbe3e9409245647019/TOOLS_JS.md
|
||||
let _decorateModuleRef = function identity(value) { return value; };
|
||||
|
||||
if ('production' === ENV) {
|
||||
// Production
|
||||
disableDebugTools();
|
||||
enableProdMode();
|
||||
|
||||
PROVIDERS = [
|
||||
...PROVIDERS,
|
||||
// custom providers in production
|
||||
];
|
||||
|
||||
} else {
|
||||
|
||||
_decorateModuleRef = (modRef: any) => {
|
||||
const appRef = modRef.injector.get(ApplicationRef);
|
||||
const cmpRef = appRef.components[0];
|
||||
|
||||
let _ng = (<any>window).ng;
|
||||
enableDebugTools(cmpRef);
|
||||
(<any>window).ng.probe = _ng.probe;
|
||||
(<any>window).ng.coreTokens = _ng.coreTokens;
|
||||
return modRef;
|
||||
};
|
||||
|
||||
// Development
|
||||
PROVIDERS = [
|
||||
...PROVIDERS,
|
||||
// custom providers in development
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
export const decorateModuleRef = _decorateModuleRef;
|
||||
|
||||
export const ENV_PROVIDERS = [
|
||||
...PROVIDERS
|
||||
];
|
@ -1,29 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Response } from '@angular/http';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { AuthHttp, AuthService } from '../shared';
|
||||
|
||||
@Injectable()
|
||||
export class FriendService {
|
||||
private static BASE_FRIEND_URL: string = '/api/v1/pods/';
|
||||
|
||||
constructor (private authHttp: AuthHttp, private authService: AuthService) {}
|
||||
|
||||
makeFriends() {
|
||||
return this.authHttp.get(FriendService.BASE_FRIEND_URL + 'makefriends')
|
||||
.map(res => res.status)
|
||||
.catch(this.handleError);
|
||||
}
|
||||
|
||||
quitFriends() {
|
||||
return this.authHttp.get(FriendService.BASE_FRIEND_URL + 'quitfriends')
|
||||
.map(res => res.status)
|
||||
.catch(this.handleError);
|
||||
}
|
||||
|
||||
private handleError (error: Response): Observable<number> {
|
||||
console.error(error);
|
||||
return Observable.throw(error.json().error || 'Server error');
|
||||
}
|
||||
}
|
1
client/src/app/index.ts
Normal file
1
client/src/app/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './app.module';
|
@ -1,17 +1,16 @@
|
||||
<h3>Login</h3>
|
||||
|
||||
|
||||
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
|
||||
|
||||
<form role="form" (ngSubmit)="login(username.value, password.value)" #loginForm="ngForm">
|
||||
<form role="form" (ngSubmit)="login()" [formGroup]="form">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input
|
||||
type="text" class="form-control" name="username" id="username" placeholder="Username" required
|
||||
ngControl="username" #username="ngForm"
|
||||
type="text" class="form-control" id="username" placeholder="Username" required
|
||||
formControlName="username"
|
||||
>
|
||||
<div [hidden]="username.valid || username.pristine" class="alert alert-danger">
|
||||
Username is required
|
||||
<div *ngIf="formErrors.username" class="alert alert-danger">
|
||||
{{ formErrors.username }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -19,12 +18,12 @@
|
||||
<label for="password">Password</label>
|
||||
<input
|
||||
type="password" class="form-control" name="password" id="password" placeholder="Password" required
|
||||
ngControl="password" #password="ngForm"
|
||||
formControlName="password"
|
||||
>
|
||||
<div [hidden]="password.valid || password.pristine" class="alert alert-danger">
|
||||
Password is required
|
||||
<div *ngIf="formErrors.password" class="alert alert-danger">
|
||||
{{ formErrors.password }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="submit" value="Login" class="btn btn-default" [disabled]="!loginForm.form.valid">
|
||||
<input type="submit" value="Login" class="btn btn-default" [disabled]="!form.valid">
|
||||
</form>
|
||||
|
@ -1,35 +1,67 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { AuthService } from '../shared';
|
||||
import { AuthService, FormReactive } from '../shared';
|
||||
|
||||
@Component({
|
||||
selector: 'my-login',
|
||||
template: require('./login.component.html')
|
||||
templateUrl: './login.component.html'
|
||||
})
|
||||
|
||||
export class LoginComponent {
|
||||
export class LoginComponent extends FormReactive implements OnInit {
|
||||
error: string = null;
|
||||
|
||||
form: FormGroup;
|
||||
formErrors = {
|
||||
'username': '',
|
||||
'password': ''
|
||||
};
|
||||
validationMessages = {
|
||||
'username': {
|
||||
'required': 'Username is required.',
|
||||
},
|
||||
'password': {
|
||||
'required': 'Password is required.'
|
||||
}
|
||||
};
|
||||
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private formBuilder: FormBuilder,
|
||||
private router: Router
|
||||
) {}
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
login(username: string, password: string) {
|
||||
this.authService.login(username, password).subscribe(
|
||||
result => {
|
||||
buildForm() {
|
||||
this.form = this.formBuilder.group({
|
||||
username: [ '', Validators.required ],
|
||||
password: [ '', Validators.required ],
|
||||
});
|
||||
|
||||
this.form.valueChanges.subscribe(data => this.onValueChanged(data));
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.buildForm();
|
||||
}
|
||||
|
||||
login() {
|
||||
this.error = null;
|
||||
|
||||
this.router.navigate(['/videos/list']);
|
||||
},
|
||||
error => {
|
||||
console.error(error);
|
||||
const { username, password } = this.form.value;
|
||||
|
||||
if (error.error === 'invalid_grant') {
|
||||
this.authService.login(username, password).subscribe(
|
||||
result => this.router.navigate(['/videos/list']),
|
||||
|
||||
error => {
|
||||
console.error(error.json);
|
||||
|
||||
if (error.json.error === 'invalid_grant') {
|
||||
this.error = 'Credentials are invalid.';
|
||||
} else {
|
||||
this.error = `${error.error}: ${error.error_description}`;
|
||||
this.error = `${error.json.error}: ${error.json.error_description}`;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
39
client/src/app/menu.component.html
Normal file
39
client/src/app/menu.component.html
Normal file
@ -0,0 +1,39 @@
|
||||
<menu class="col-md-2 col-sm-3 col-xs-3">
|
||||
<div class="panel-block">
|
||||
<div id="panel-user-login" class="panel-button">
|
||||
<span *ngIf="!isLoggedIn" >
|
||||
<span class="hidden-xs glyphicon glyphicon-log-in"></span>
|
||||
<a [routerLink]="['/login']">Login</a>
|
||||
</span>
|
||||
|
||||
<span *ngIf="isLoggedIn">
|
||||
<span class="hidden-xs glyphicon glyphicon-log-out"></span>
|
||||
<a *ngIf="isLoggedIn" (click)="logout()">Logout</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isLoggedIn" id="panel-user-account" class="panel-button">
|
||||
<span class="hidden-xs glyphicon glyphicon-user"></span>
|
||||
<a [routerLink]="['/account']">My account</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-block">
|
||||
<div id="panel-get-videos" class="panel-button">
|
||||
<span class="hidden-xs glyphicon glyphicon-list"></span>
|
||||
<a [routerLink]="['/videos/list']">Get videos</a>
|
||||
</div>
|
||||
|
||||
<div id="panel-upload-video" class="panel-button" *ngIf="isLoggedIn">
|
||||
<span class="hidden-xs glyphicon glyphicon-cloud-upload"></span>
|
||||
<a [routerLink]="['/videos/add']">Upload a video</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-block" *ngIf="isUserAdmin()">
|
||||
<div id="panel-get-videos" class="panel-button">
|
||||
<span class="hidden-xs glyphicon glyphicon-cog"></span>
|
||||
<a [routerLink]="['/admin']">Administration</a>
|
||||
</div>
|
||||
</div>
|
||||
</menu>
|
45
client/src/app/menu.component.ts
Normal file
45
client/src/app/menu.component.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { AuthService, AuthStatus } from './shared';
|
||||
|
||||
@Component({
|
||||
selector: 'my-menu',
|
||||
templateUrl: './menu.component.html'
|
||||
})
|
||||
export class MenuComponent implements OnInit {
|
||||
isLoggedIn: boolean;
|
||||
|
||||
constructor (
|
||||
private authService: AuthService,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.isLoggedIn = this.authService.isLoggedIn();
|
||||
|
||||
this.authService.loginChangedSource.subscribe(
|
||||
status => {
|
||||
if (status === AuthStatus.LoggedIn) {
|
||||
this.isLoggedIn = true;
|
||||
console.log('Logged in.');
|
||||
} else if (status === AuthStatus.LoggedOut) {
|
||||
this.isLoggedIn = false;
|
||||
console.log('Logged out.');
|
||||
} else {
|
||||
console.error('Unknown auth status: ' + status);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
isUserAdmin() {
|
||||
return this.authService.isAdmin();
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.authService.logout();
|
||||
// Redirect to home page
|
||||
this.router.navigate(['/videos/list']);
|
||||
}
|
||||
}
|
@ -28,7 +28,7 @@ export class AuthHttp extends Http {
|
||||
return super.request(url, options)
|
||||
.catch((err) => {
|
||||
if (err.status === 401) {
|
||||
return this.handleTokenExpired(err, url, options);
|
||||
return this.handleTokenExpired(url, options);
|
||||
}
|
||||
|
||||
return Observable.throw(err);
|
||||
@ -49,22 +49,25 @@ export class AuthHttp extends Http {
|
||||
return this.request(url, options);
|
||||
}
|
||||
|
||||
post(url: string, options?: RequestOptionsArgs): Observable<Response> {
|
||||
post(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
|
||||
if (!options) options = {};
|
||||
options.method = RequestMethod.Post;
|
||||
options.body = body;
|
||||
|
||||
return this.request(url, options);
|
||||
}
|
||||
|
||||
put(url: string, options?: RequestOptionsArgs): Observable<Response> {
|
||||
put(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
|
||||
if (!options) options = {};
|
||||
options.method = RequestMethod.Put;
|
||||
options.body = body;
|
||||
|
||||
return this.request(url, options);
|
||||
}
|
||||
|
||||
private handleTokenExpired(err: Response, url: string | Request, options: RequestOptionsArgs) {
|
||||
return this.authService.refreshAccessToken().flatMap(() => {
|
||||
private handleTokenExpired(url: string | Request, options: RequestOptionsArgs) {
|
||||
return this.authService.refreshAccessToken()
|
||||
.flatMap(() => {
|
||||
this.setAuthorizationHeader(options.headers);
|
||||
|
||||
return super.request(url, options);
|
||||
|
@ -1,15 +1,28 @@
|
||||
export class User {
|
||||
import { User } from '../users';
|
||||
|
||||
export class AuthUser extends User {
|
||||
private static KEYS = {
|
||||
ID: 'id',
|
||||
ROLE: 'role',
|
||||
USERNAME: 'username'
|
||||
};
|
||||
|
||||
id: string;
|
||||
role: string;
|
||||
username: string;
|
||||
tokens: Tokens;
|
||||
|
||||
static load() {
|
||||
const usernameLocalStorage = localStorage.getItem(this.KEYS.USERNAME);
|
||||
if (usernameLocalStorage) {
|
||||
return new User(localStorage.getItem(this.KEYS.USERNAME), Tokens.load());
|
||||
return new AuthUser(
|
||||
{
|
||||
id: localStorage.getItem(this.KEYS.ID),
|
||||
username: localStorage.getItem(this.KEYS.USERNAME),
|
||||
role: localStorage.getItem(this.KEYS.ROLE)
|
||||
},
|
||||
Tokens.load()
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -17,12 +30,14 @@ export class User {
|
||||
|
||||
static flush() {
|
||||
localStorage.removeItem(this.KEYS.USERNAME);
|
||||
localStorage.removeItem(this.KEYS.ID);
|
||||
localStorage.removeItem(this.KEYS.ROLE);
|
||||
Tokens.flush();
|
||||
}
|
||||
|
||||
constructor(username: string, hash_tokens: any) {
|
||||
this.username = username;
|
||||
this.tokens = new Tokens(hash_tokens);
|
||||
constructor(userHash: { id: string, username: string, role: string }, hashTokens: any) {
|
||||
super(userHash);
|
||||
this.tokens = new Tokens(hashTokens);
|
||||
}
|
||||
|
||||
getAccessToken() {
|
||||
@ -43,12 +58,14 @@ export class User {
|
||||
}
|
||||
|
||||
save() {
|
||||
localStorage.setItem('username', this.username);
|
||||
localStorage.setItem(AuthUser.KEYS.ID, this.id);
|
||||
localStorage.setItem(AuthUser.KEYS.USERNAME, this.username);
|
||||
localStorage.setItem(AuthUser.KEYS.ROLE, this.role);
|
||||
this.tokens.save();
|
||||
}
|
||||
}
|
||||
|
||||
// Private class used only by User
|
||||
// Private class only used by User
|
||||
class Tokens {
|
||||
private static KEYS = {
|
||||
ACCESS_TOKEN: 'access_token',
|
@ -1,32 +1,39 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Headers, Http, Response, URLSearchParams } from '@angular/http';
|
||||
import { Router } from '@angular/router';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
|
||||
import { AuthStatus } from './auth-status.model';
|
||||
import { User } from './user.model';
|
||||
import { AuthUser } from './auth-user.model';
|
||||
import { RestExtractor } from '../rest';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
private static BASE_CLIENT_URL = '/api/v1/users/client';
|
||||
private static BASE_CLIENT_URL = '/api/v1/clients/local';
|
||||
private static BASE_TOKEN_URL = '/api/v1/users/token';
|
||||
private static BASE_USER_INFORMATIONS_URL = '/api/v1/users/me';
|
||||
|
||||
loginChangedSource: Observable<AuthStatus>;
|
||||
|
||||
private clientId: string;
|
||||
private clientSecret: string;
|
||||
private loginChanged: Subject<AuthStatus>;
|
||||
private user: User = null;
|
||||
private user: AuthUser = null;
|
||||
|
||||
constructor(private http: Http) {
|
||||
constructor(
|
||||
private http: Http,
|
||||
private restExtractor: RestExtractor,
|
||||
private router: Router
|
||||
) {
|
||||
this.loginChanged = new Subject<AuthStatus>();
|
||||
this.loginChangedSource = this.loginChanged.asObservable();
|
||||
|
||||
// Fetch the client_id/client_secret
|
||||
// FIXME: save in local storage?
|
||||
this.http.get(AuthService.BASE_CLIENT_URL)
|
||||
.map(res => res.json())
|
||||
.catch(this.handleError)
|
||||
.map(this.restExtractor.extractDataGet)
|
||||
.catch((res) => this.restExtractor.handleError(res))
|
||||
.subscribe(
|
||||
result => {
|
||||
this.clientId = result.client_id;
|
||||
@ -34,12 +41,15 @@ export class AuthService {
|
||||
console.log('Client credentials loaded.');
|
||||
},
|
||||
error => {
|
||||
alert(error);
|
||||
alert(
|
||||
`Cannot retrieve OAuth Client credentials: ${error.text}. \n` +
|
||||
'Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.'
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Return null if there is nothing to load
|
||||
this.user = User.load();
|
||||
this.user = AuthUser.load();
|
||||
}
|
||||
|
||||
getRefreshToken() {
|
||||
@ -64,10 +74,16 @@ export class AuthService {
|
||||
return this.user.getTokenType();
|
||||
}
|
||||
|
||||
getUser(): User {
|
||||
getUser(): AuthUser {
|
||||
return this.user;
|
||||
}
|
||||
|
||||
isAdmin() {
|
||||
if (this.user === null) return false;
|
||||
|
||||
return this.user.isAdmin();
|
||||
}
|
||||
|
||||
isLoggedIn() {
|
||||
if (this.getAccessToken()) {
|
||||
return true;
|
||||
@ -94,21 +110,23 @@ export class AuthService {
|
||||
};
|
||||
|
||||
return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options)
|
||||
.map(res => res.json())
|
||||
.map(this.restExtractor.extractDataGet)
|
||||
.map(res => {
|
||||
res.username = username;
|
||||
return res;
|
||||
})
|
||||
.flatMap(res => this.fetchUserInformations(res))
|
||||
.map(res => this.handleLogin(res))
|
||||
.catch(this.handleError);
|
||||
.catch((res) => this.restExtractor.handleError(res));
|
||||
}
|
||||
|
||||
logout() {
|
||||
// TODO: make an HTTP request to revoke the tokens
|
||||
this.user = null;
|
||||
User.flush();
|
||||
|
||||
this.setStatus(AuthStatus.LoggedIn);
|
||||
AuthUser.flush();
|
||||
|
||||
this.setStatus(AuthStatus.LoggedOut);
|
||||
}
|
||||
|
||||
refreshAccessToken() {
|
||||
@ -131,36 +149,64 @@ export class AuthService {
|
||||
};
|
||||
|
||||
return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options)
|
||||
.map(res => res.json())
|
||||
.map(this.restExtractor.extractDataGet)
|
||||
.map(res => this.handleRefreshToken(res))
|
||||
.catch(this.handleError);
|
||||
.catch((res: Response) => {
|
||||
// The refresh token is invalid?
|
||||
if (res.status === 400 && res.json() && res.json().error === 'invalid_grant') {
|
||||
console.error('Cannot refresh token -> logout...');
|
||||
this.logout();
|
||||
this.router.navigate(['/login']);
|
||||
|
||||
return Observable.throw({
|
||||
json: '',
|
||||
text: 'You need to reconnect.'
|
||||
});
|
||||
}
|
||||
|
||||
private setStatus(status: AuthStatus) {
|
||||
this.loginChanged.next(status);
|
||||
return this.restExtractor.handleError(res);
|
||||
});
|
||||
}
|
||||
|
||||
private fetchUserInformations (obj: any) {
|
||||
// Do not call authHttp here to avoid circular dependencies headaches
|
||||
|
||||
const headers = new Headers();
|
||||
headers.set('Authorization', `Bearer ${obj.access_token}`);
|
||||
|
||||
return this.http.get(AuthService.BASE_USER_INFORMATIONS_URL, { headers })
|
||||
.map(res => res.json())
|
||||
.map(res => {
|
||||
obj.id = res.id;
|
||||
obj.role = res.role;
|
||||
return obj;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private handleLogin (obj: any) {
|
||||
const id = obj.id;
|
||||
const username = obj.username;
|
||||
const hash_tokens = {
|
||||
const role = obj.role;
|
||||
const hashTokens = {
|
||||
access_token: obj.access_token,
|
||||
token_type: obj.token_type,
|
||||
refresh_token: obj.refresh_token
|
||||
};
|
||||
|
||||
this.user = new User(username, hash_tokens);
|
||||
this.user = new AuthUser({ id, username, role }, hashTokens);
|
||||
this.user.save();
|
||||
|
||||
this.setStatus(AuthStatus.LoggedIn);
|
||||
}
|
||||
|
||||
private handleError (error: Response) {
|
||||
console.error(error);
|
||||
return Observable.throw(error.json() || { error: 'Server error' });
|
||||
}
|
||||
|
||||
private handleRefreshToken (obj: any) {
|
||||
this.user.refreshTokens(obj.access_token, obj.refresh_token);
|
||||
this.user.save();
|
||||
}
|
||||
|
||||
private setStatus(status: AuthStatus) {
|
||||
this.loginChanged.next(status);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
export * from './auth-http.service';
|
||||
export * from './auth-status.model';
|
||||
export * from './auth.service';
|
||||
export * from './user.model';
|
||||
export * from './auth-user.model';
|
||||
|
24
client/src/app/shared/forms/form-reactive.ts
Normal file
24
client/src/app/shared/forms/form-reactive.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { FormGroup } from '@angular/forms';
|
||||
|
||||
export abstract class FormReactive {
|
||||
abstract form: FormGroup;
|
||||
abstract formErrors: Object;
|
||||
abstract validationMessages: Object;
|
||||
|
||||
abstract buildForm(): void;
|
||||
|
||||
protected onValueChanged(data?: any) {
|
||||
for (const field in this.formErrors) {
|
||||
// clear previous error message (if any)
|
||||
this.formErrors[field] = '';
|
||||
const control = this.form.get(field);
|
||||
|
||||
if (control && control.dirty && !control.valid) {
|
||||
const messages = this.validationMessages[field];
|
||||
for (const key in control.errors) {
|
||||
this.formErrors[field] += messages[key] + ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
3
client/src/app/shared/forms/form-validators/index.ts
Normal file
3
client/src/app/shared/forms/form-validators/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './url.validator';
|
||||
export * from './user';
|
||||
export * from './video';
|
11
client/src/app/shared/forms/form-validators/url.validator.ts
Normal file
11
client/src/app/shared/forms/form-validators/url.validator.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { FormControl } from '@angular/forms';
|
||||
|
||||
export function validateUrl(c: FormControl) {
|
||||
let URL_REGEXP = new RegExp('^https?://(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$');
|
||||
|
||||
return URL_REGEXP.test(c.value) ? null : {
|
||||
validateUrl: {
|
||||
valid: false
|
||||
}
|
||||
};
|
||||
}
|
17
client/src/app/shared/forms/form-validators/user.ts
Normal file
17
client/src/app/shared/forms/form-validators/user.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Validators } from '@angular/forms';
|
||||
|
||||
export const USER_USERNAME = {
|
||||
VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(20) ],
|
||||
MESSAGES: {
|
||||
'required': 'Username is required.',
|
||||
'minlength': 'Username must be at least 3 characters long.',
|
||||
'maxlength': 'Username cannot be more than 20 characters long.'
|
||||
}
|
||||
};
|
||||
export const USER_PASSWORD = {
|
||||
VALIDATORS: [ Validators.required, Validators.minLength(6) ],
|
||||
MESSAGES: {
|
||||
'required': 'Password is required.',
|
||||
'minlength': 'Password must be at least 6 characters long.',
|
||||
}
|
||||
};
|
25
client/src/app/shared/forms/form-validators/video.ts
Normal file
25
client/src/app/shared/forms/form-validators/video.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Validators } from '@angular/forms';
|
||||
|
||||
export const VIDEO_NAME = {
|
||||
VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(50) ],
|
||||
MESSAGES: {
|
||||
'required': 'Video name is required.',
|
||||
'minlength': 'Video name must be at least 3 characters long.',
|
||||
'maxlength': 'Video name cannot be more than 50 characters long.'
|
||||
}
|
||||
};
|
||||
export const VIDEO_DESCRIPTION = {
|
||||
VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(250) ],
|
||||
MESSAGES: {
|
||||
'required': 'Video description is required.',
|
||||
'minlength': 'Video description must be at least 3 characters long.',
|
||||
'maxlength': 'Video description cannot be more than 250 characters long.'
|
||||
}
|
||||
};
|
||||
|
||||
export const VIDEO_TAGS = {
|
||||
VALIDATORS: [ Validators.pattern('^[a-zA-Z0-9]{2,10}$') ],
|
||||
MESSAGES: {
|
||||
'pattern': 'A tag should be between 2 and 10 alphanumeric characters long.'
|
||||
}
|
||||
};
|
2
client/src/app/shared/forms/index.ts
Normal file
2
client/src/app/shared/forms/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './form-validators';
|
||||
export * from './form-reactive';
|
@ -1,2 +1,5 @@
|
||||
export * from './auth';
|
||||
export * from './forms';
|
||||
export * from './rest';
|
||||
export * from './search';
|
||||
export * from './users';
|
||||
|
3
client/src/app/shared/rest/index.ts
Normal file
3
client/src/app/shared/rest/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './rest-extractor.service';
|
||||
export * from './rest-pagination';
|
||||
export * from './rest.service';
|
52
client/src/app/shared/rest/rest-extractor.service.ts
Normal file
52
client/src/app/shared/rest/rest-extractor.service.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Response } from '@angular/http';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
export interface ResultList {
|
||||
data: any[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class RestExtractor {
|
||||
|
||||
constructor () { ; }
|
||||
|
||||
extractDataBool(res: Response) {
|
||||
return true;
|
||||
}
|
||||
|
||||
extractDataList(res: Response) {
|
||||
const body = res.json();
|
||||
|
||||
const ret: ResultList = {
|
||||
data: body.data,
|
||||
total: body.total
|
||||
};
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
extractDataGet(res: Response) {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
handleError(res: Response) {
|
||||
let text = 'Server error: ';
|
||||
text += res.text();
|
||||
let json = '';
|
||||
|
||||
try {
|
||||
json = res.json();
|
||||
} catch (err) { ; }
|
||||
|
||||
const error = {
|
||||
json,
|
||||
text
|
||||
};
|
||||
|
||||
console.error(error);
|
||||
|
||||
return Observable.throw(error);
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
export interface Pagination {
|
||||
export interface RestPagination {
|
||||
currentPage: number;
|
||||
itemsPerPage: number;
|
||||
totalItems: number;
|
||||
}
|
||||
};
|
27
client/src/app/shared/rest/rest.service.ts
Normal file
27
client/src/app/shared/rest/rest.service.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { URLSearchParams } from '@angular/http';
|
||||
|
||||
import { RestPagination } from './rest-pagination';
|
||||
|
||||
@Injectable()
|
||||
export class RestService {
|
||||
|
||||
buildRestGetParams(pagination?: RestPagination, sort?: string) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (pagination) {
|
||||
const start: number = (pagination.currentPage - 1) * pagination.itemsPerPage;
|
||||
const count: number = pagination.itemsPerPage;
|
||||
|
||||
params.set('start', start.toString());
|
||||
params.set('count', count.toString());
|
||||
}
|
||||
|
||||
if (sort) {
|
||||
params.set('sort', sort);
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
import { DROPDOWN_DIRECTIVES} from 'ng2-bootstrap/components/dropdown';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { Search } from './search.model';
|
||||
import { SearchField } from './search-field.type';
|
||||
@ -8,8 +7,7 @@ import { SearchService } from './search.service';
|
||||
|
||||
@Component({
|
||||
selector: 'my-search',
|
||||
template: require('./search.component.html'),
|
||||
directives: [ DROPDOWN_DIRECTIVES ]
|
||||
templateUrl: './search.component.html'
|
||||
})
|
||||
|
||||
export class SearchComponent implements OnInit {
|
||||
@ -25,10 +23,10 @@ export class SearchComponent implements OnInit {
|
||||
value: ''
|
||||
};
|
||||
|
||||
constructor(private searchService: SearchService) {}
|
||||
constructor(private searchService: SearchService, private router: Router) {}
|
||||
|
||||
ngOnInit() {
|
||||
// Subscribe is the search changed
|
||||
// Subscribe if the search changed
|
||||
// Usually changed by videos list component
|
||||
this.searchService.updateSearch.subscribe(
|
||||
newSearchCriterias => {
|
||||
@ -58,6 +56,10 @@ export class SearchComponent implements OnInit {
|
||||
}
|
||||
|
||||
doSearch() {
|
||||
if (this.router.url.indexOf('/videos/list') === -1) {
|
||||
this.router.navigate([ '/videos/list' ]);
|
||||
}
|
||||
|
||||
this.searchService.searchUpdated.next(this.searchCriterias);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
import { ReplaySubject } from 'rxjs/ReplaySubject';
|
||||
|
||||
import { Search } from './search.model';
|
||||
|
||||
@ -12,6 +13,6 @@ export class SearchService {
|
||||
|
||||
constructor() {
|
||||
this.updateSearch = new Subject<Search>();
|
||||
this.searchUpdated = new Subject<Search>();
|
||||
this.searchUpdated = new ReplaySubject<Search>(1);
|
||||
}
|
||||
}
|
||||
|
1
client/src/app/shared/users/index.ts
Normal file
1
client/src/app/shared/users/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './user.model';
|
20
client/src/app/shared/users/user.model.ts
Normal file
20
client/src/app/shared/users/user.model.ts
Normal file
@ -0,0 +1,20 @@
|
||||
export class User {
|
||||
id: string;
|
||||
username: string;
|
||||
role: string;
|
||||
createdDate: Date;
|
||||
|
||||
constructor(hash: { id: string, username: string, role: string, createdDate?: Date }) {
|
||||
this.id = hash.id;
|
||||
this.username = hash.username;
|
||||
this.role = hash.role;
|
||||
|
||||
if (hash.createdDate) {
|
||||
this.createdDate = hash.createdDate;
|
||||
}
|
||||
}
|
||||
|
||||
isAdmin() {
|
||||
return this.role === 'admin';
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
export * from './loader';
|
||||
export * from './pagination.model';
|
||||
export * from './sort-field.type';
|
||||
export * from './video.model';
|
||||
export * from './video.service';
|
||||
|
@ -2,8 +2,8 @@ import { Component, Input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'my-loader',
|
||||
styles: [ require('./loader.component.scss') ],
|
||||
template: require('./loader.component.html')
|
||||
styleUrls: [ './loader.component.scss' ],
|
||||
templateUrl: './loader.component.html'
|
||||
})
|
||||
|
||||
export class LoaderComponent {
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Http, Response, URLSearchParams } from '@angular/http';
|
||||
import { Http } from '@angular/http';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { Pagination } from './pagination.model';
|
||||
import { Search } from '../../shared';
|
||||
import { SortField } from './sort-field.type';
|
||||
import { AuthHttp, AuthService } from '../../shared';
|
||||
import { AuthHttp, AuthService, RestExtractor, RestPagination, RestService, ResultList } from '../../shared';
|
||||
import { Video } from './video.model';
|
||||
|
||||
@Injectable()
|
||||
@ -15,68 +14,51 @@ export class VideoService {
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private authHttp: AuthHttp,
|
||||
private http: Http
|
||||
private http: Http,
|
||||
private restExtractor: RestExtractor,
|
||||
private restService: RestService
|
||||
) {}
|
||||
|
||||
getVideo(id: string) {
|
||||
getVideo(id: string): Observable<Video> {
|
||||
return this.http.get(VideoService.BASE_VIDEO_URL + id)
|
||||
.map(res => <Video> res.json())
|
||||
.catch(this.handleError);
|
||||
.map(this.restExtractor.extractDataGet)
|
||||
.catch((res) => this.restExtractor.handleError(res));
|
||||
}
|
||||
|
||||
getVideos(pagination: Pagination, sort: SortField) {
|
||||
const params = this.createPaginationParams(pagination);
|
||||
|
||||
if (sort) params.set('sort', sort);
|
||||
getVideos(pagination: RestPagination, sort: SortField) {
|
||||
const params = this.restService.buildRestGetParams(pagination, sort);
|
||||
|
||||
return this.http.get(VideoService.BASE_VIDEO_URL, { search: params })
|
||||
.map(res => res.json())
|
||||
.map(this.extractVideos)
|
||||
.catch(this.handleError);
|
||||
.catch((res) => this.restExtractor.handleError(res));
|
||||
}
|
||||
|
||||
removeVideo(id: string) {
|
||||
return this.authHttp.delete(VideoService.BASE_VIDEO_URL + id)
|
||||
.map(res => <number> res.status)
|
||||
.catch(this.handleError);
|
||||
.map(this.restExtractor.extractDataBool)
|
||||
.catch((res) => this.restExtractor.handleError(res));
|
||||
}
|
||||
|
||||
searchVideos(search: Search, pagination: Pagination, sort: SortField) {
|
||||
const params = this.createPaginationParams(pagination);
|
||||
searchVideos(search: Search, pagination: RestPagination, sort: SortField) {
|
||||
const params = this.restService.buildRestGetParams(pagination, sort);
|
||||
|
||||
if (search.field) params.set('field', search.field);
|
||||
if (sort) params.set('sort', sort);
|
||||
|
||||
return this.http.get(VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value), { search: params })
|
||||
.map(res => res.json())
|
||||
.map(this.restExtractor.extractDataList)
|
||||
.map(this.extractVideos)
|
||||
.catch(this.handleError);
|
||||
.catch((res) => this.restExtractor.handleError(res));
|
||||
}
|
||||
|
||||
private createPaginationParams(pagination: Pagination) {
|
||||
const params = new URLSearchParams();
|
||||
const start: number = (pagination.currentPage - 1) * pagination.itemsPerPage;
|
||||
const count: number = pagination.itemsPerPage;
|
||||
|
||||
params.set('start', start.toString());
|
||||
params.set('count', count.toString());
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
private extractVideos(body: any) {
|
||||
const videos_json = body.data;
|
||||
const totalVideos = body.total;
|
||||
private extractVideos(result: ResultList) {
|
||||
const videosJson = result.data;
|
||||
const totalVideos = result.total;
|
||||
const videos = [];
|
||||
for (const video_json of videos_json) {
|
||||
videos.push(new Video(video_json));
|
||||
for (const videoJson of videosJson) {
|
||||
videos.push(new Video(videoJson));
|
||||
}
|
||||
|
||||
return { videos, totalVideos };
|
||||
}
|
||||
|
||||
private handleError(error: Response) {
|
||||
console.error(error);
|
||||
return Observable.throw(error.json().error || 'Server error');
|
||||
}
|
||||
}
|
||||
|
@ -2,31 +2,31 @@
|
||||
|
||||
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
|
||||
|
||||
<form novalidate (ngSubmit)="upload()" [ngFormModel]="videoForm">
|
||||
<form novalidate (ngSubmit)="upload()" [formGroup]="form">
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<input
|
||||
type="text" class="form-control" name="name" id="name"
|
||||
ngControl="name" #name="ngForm" [(ngModel)]="video.name"
|
||||
type="text" class="form-control" id="name"
|
||||
formControlName="name"
|
||||
>
|
||||
<div [hidden]="name.valid || name.pristine" class="alert alert-warning">
|
||||
A name is required and should be between 3 and 50 characters long
|
||||
<div *ngIf="formErrors.name" class="alert alert-danger">
|
||||
{{ formErrors.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="tags">Tags</label>
|
||||
<input
|
||||
type="text" class="form-control" name="tags" id="tags"
|
||||
ngControl="tags" #tags="ngForm" [disabled]="isTagsInputDisabled" (keyup)="onTagKeyPress($event)" [(ngModel)]="currentTag"
|
||||
type="text" class="form-control" id="currentTag"
|
||||
formControlName="currentTag" (keyup)="onTagKeyPress($event)"
|
||||
>
|
||||
<div [hidden]="tags.valid || tags.pristine" class="alert alert-warning">
|
||||
A tag should be between 2 and 10 characters (alphanumeric) long
|
||||
<div *ngIf="formErrors.currentTag" class="alert alert-danger">
|
||||
{{ formErrors.currentTag }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tags">
|
||||
<div class="label label-primary tag" *ngFor="let tag of video.tags">
|
||||
<div class="label label-primary tag" *ngFor="let tag of tags">
|
||||
{{ tag }}
|
||||
<span class="remove" (click)="removeTag(tag)">x</span>
|
||||
</div>
|
||||
@ -53,12 +53,12 @@
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea
|
||||
name="description" id="description" class="form-control" placeholder="Description..."
|
||||
ngControl="description" #description="ngForm" [(ngModel)]="video.description"
|
||||
id="description" class="form-control" placeholder="Description..."
|
||||
formControlName="description"
|
||||
>
|
||||
</textarea>
|
||||
<div [hidden]="description.valid || description.pristine" class="alert alert-warning">
|
||||
A description is required and should be between 3 and 250 characters long
|
||||
<div *ngIf="formErrors.description" class="alert alert-danger">
|
||||
{{ formErrors.description }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -69,7 +69,7 @@
|
||||
<div class="form-group">
|
||||
<input
|
||||
type="submit" value="Upload" class="btn btn-default form-control" [title]="getInvalidFieldsTitle()"
|
||||
[disabled]="!videoForm.valid || video.tags.length === 0 || filename === null"
|
||||
[disabled]="!form.valid || tags.length === 0 || filename === null"
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -1,37 +1,42 @@
|
||||
import { Control, ControlGroup, Validators } from '@angular/common';
|
||||
import { Component, ElementRef, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe';
|
||||
import { PROGRESSBAR_DIRECTIVES } from 'ng2-bootstrap/components/progressbar';
|
||||
import { FileSelectDirective, FileUploader } from 'ng2-file-upload/ng2-file-upload';
|
||||
import { FileUploader } from 'ng2-file-upload/ng2-file-upload';
|
||||
|
||||
import { AuthService } from '../../shared';
|
||||
import { AuthService, FormReactive, VIDEO_NAME, VIDEO_DESCRIPTION, VIDEO_TAGS } from '../../shared';
|
||||
|
||||
@Component({
|
||||
selector: 'my-videos-add',
|
||||
styles: [ require('./video-add.component.scss') ],
|
||||
template: require('./video-add.component.html'),
|
||||
directives: [ FileSelectDirective, PROGRESSBAR_DIRECTIVES ],
|
||||
pipes: [ BytesPipe ]
|
||||
styleUrls: [ './video-add.component.scss' ],
|
||||
templateUrl: './video-add.component.html'
|
||||
})
|
||||
|
||||
export class VideoAddComponent implements OnInit {
|
||||
currentTag: string; // Tag the user is writing in the input
|
||||
error: string = null;
|
||||
videoForm: ControlGroup;
|
||||
export class VideoAddComponent extends FormReactive implements OnInit {
|
||||
tags: string[] = [];
|
||||
uploader: FileUploader;
|
||||
video = {
|
||||
|
||||
error: string = null;
|
||||
form: FormGroup;
|
||||
formErrors = {
|
||||
name: '',
|
||||
tags: [],
|
||||
description: ''
|
||||
description: '',
|
||||
currentTag: ''
|
||||
};
|
||||
validationMessages = {
|
||||
name: VIDEO_NAME.MESSAGES,
|
||||
description: VIDEO_DESCRIPTION.MESSAGES,
|
||||
currentTag: VIDEO_TAGS.MESSAGES
|
||||
};
|
||||
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private elementRef: ElementRef,
|
||||
private formBuilder: FormBuilder,
|
||||
private router: Router
|
||||
) {}
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
get filename() {
|
||||
if (this.uploader.queue.length === 0) {
|
||||
@ -41,20 +46,26 @@ export class VideoAddComponent implements OnInit {
|
||||
return this.uploader.queue[0].file.name;
|
||||
}
|
||||
|
||||
get isTagsInputDisabled () {
|
||||
return this.video.tags.length >= 3;
|
||||
buildForm() {
|
||||
this.form = this.formBuilder.group({
|
||||
name: [ '', VIDEO_NAME.VALIDATORS ],
|
||||
description: [ '', VIDEO_DESCRIPTION.VALIDATORS ],
|
||||
currentTag: [ '', VIDEO_TAGS.VALIDATORS ]
|
||||
});
|
||||
|
||||
this.form.valueChanges.subscribe(data => this.onValueChanged(data));
|
||||
}
|
||||
|
||||
getInvalidFieldsTitle() {
|
||||
let title = '';
|
||||
const nameControl = this.videoForm.controls['name'];
|
||||
const descriptionControl = this.videoForm.controls['description'];
|
||||
const nameControl = this.form.controls['name'];
|
||||
const descriptionControl = this.form.controls['description'];
|
||||
|
||||
if (!nameControl.valid) {
|
||||
title += 'A name is required\n';
|
||||
}
|
||||
|
||||
if (this.video.tags.length === 0) {
|
||||
if (this.tags.length === 0) {
|
||||
title += 'At least one tag is required\n';
|
||||
}
|
||||
|
||||
@ -70,13 +81,6 @@ export class VideoAddComponent implements OnInit {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.videoForm = new ControlGroup({
|
||||
name: new Control('', Validators.compose([ Validators.required, Validators.minLength(3), Validators.maxLength(50) ])),
|
||||
description: new Control('', Validators.compose([ Validators.required, Validators.minLength(3), Validators.maxLength(250) ])),
|
||||
tags: new Control('', Validators.pattern('^[a-zA-Z0-9]{2,10}$'))
|
||||
});
|
||||
|
||||
|
||||
this.uploader = new FileUploader({
|
||||
authToken: this.authService.getRequestHeaderValue(),
|
||||
queueLimit: 1,
|
||||
@ -85,26 +89,37 @@ export class VideoAddComponent implements OnInit {
|
||||
});
|
||||
|
||||
this.uploader.onBuildItemForm = (item, form) => {
|
||||
form.append('name', this.video.name);
|
||||
form.append('description', this.video.description);
|
||||
const name = this.form.value['name'];
|
||||
const description = this.form.value['description'];
|
||||
|
||||
for (let i = 0; i < this.video.tags.length; i++) {
|
||||
form.append(`tags[${i}]`, this.video.tags[i]);
|
||||
form.append('name', name);
|
||||
form.append('description', description);
|
||||
|
||||
for (let i = 0; i < this.tags.length; i++) {
|
||||
form.append(`tags[${i}]`, this.tags[i]);
|
||||
}
|
||||
};
|
||||
|
||||
this.buildForm();
|
||||
}
|
||||
|
||||
onTagKeyPress(event: KeyboardEvent) {
|
||||
const currentTag = this.form.value['currentTag'];
|
||||
|
||||
// Enter press
|
||||
if (event.keyCode === 13) {
|
||||
// Check if the tag is valid and does not already exist
|
||||
if (
|
||||
this.currentTag !== '' &&
|
||||
this.videoForm.controls['tags'].valid &&
|
||||
this.video.tags.indexOf(this.currentTag) === -1
|
||||
currentTag !== '' &&
|
||||
this.form.controls['currentTag'].valid &&
|
||||
this.tags.indexOf(currentTag) === -1
|
||||
) {
|
||||
this.video.tags.push(this.currentTag);
|
||||
this.currentTag = '';
|
||||
this.tags.push(currentTag);
|
||||
this.form.patchValue({ currentTag: '' });
|
||||
|
||||
if (this.tags.length >= 3) {
|
||||
this.form.get('currentTag').disable();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -114,7 +129,7 @@ export class VideoAddComponent implements OnInit {
|
||||
}
|
||||
|
||||
removeTag(tag: string) {
|
||||
this.video.tags.splice(this.video.tags.indexOf(tag), 1);
|
||||
this.tags.splice(this.tags.indexOf(tag), 1);
|
||||
}
|
||||
|
||||
upload() {
|
||||
|
@ -1,39 +1,30 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { ActivatedRoute, Router, ROUTER_DIRECTIVES } from '@angular/router';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
|
||||
import { PAGINATION_DIRECTIVES } from 'ng2-bootstrap/components/pagination';
|
||||
|
||||
import {
|
||||
LoaderComponent,
|
||||
Pagination,
|
||||
SortField,
|
||||
Video,
|
||||
VideoService
|
||||
} from '../shared';
|
||||
import { AuthService, Search, SearchField, User } from '../../shared';
|
||||
import { VideoMiniatureComponent } from './video-miniature.component';
|
||||
import { VideoSortComponent } from './video-sort.component';
|
||||
import { AuthService, AuthUser, RestPagination, Search, SearchField } from '../../shared';
|
||||
import { SearchService } from '../../shared';
|
||||
|
||||
@Component({
|
||||
selector: 'my-videos-list',
|
||||
styles: [ require('./video-list.component.scss') ],
|
||||
pipes: [ AsyncPipe ],
|
||||
template: require('./video-list.component.html'),
|
||||
directives: [ LoaderComponent, PAGINATION_DIRECTIVES, ROUTER_DIRECTIVES, VideoMiniatureComponent, VideoSortComponent ]
|
||||
styleUrls: [ './video-list.component.scss' ],
|
||||
templateUrl: './video-list.component.html'
|
||||
})
|
||||
|
||||
export class VideoListComponent implements OnInit, OnDestroy {
|
||||
loading: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||
pagination: Pagination = {
|
||||
pagination: RestPagination = {
|
||||
currentPage: 1,
|
||||
itemsPerPage: 9,
|
||||
totalItems: null
|
||||
};
|
||||
sort: SortField;
|
||||
user: User = null;
|
||||
user: AuthUser = null;
|
||||
videos: Video[] = [];
|
||||
|
||||
private search: Search;
|
||||
@ -51,7 +42,7 @@ export class VideoListComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit() {
|
||||
if (this.authService.isLoggedIn()) {
|
||||
this.user = User.load();
|
||||
this.user = AuthUser.load();
|
||||
}
|
||||
|
||||
// Subscribe to route changes
|
||||
@ -66,6 +57,8 @@ export class VideoListComponent implements OnInit, OnDestroy {
|
||||
// Subscribe to search changes
|
||||
this.subSearch = this.searchService.searchUpdated.subscribe(search => {
|
||||
this.search = search;
|
||||
// Reset pagination
|
||||
this.pagination.currentPage = 1;
|
||||
|
||||
this.navigateToNewParams();
|
||||
});
|
||||
@ -76,7 +69,7 @@ export class VideoListComponent implements OnInit, OnDestroy {
|
||||
this.subSearch.unsubscribe();
|
||||
}
|
||||
|
||||
getVideos(detectChanges = true) {
|
||||
getVideos() {
|
||||
this.loading.next(true);
|
||||
this.videos = [];
|
||||
|
||||
@ -97,7 +90,7 @@ export class VideoListComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.loading.next(false);
|
||||
},
|
||||
error => alert(error)
|
||||
error => alert(error.text)
|
||||
);
|
||||
}
|
||||
|
||||
@ -153,7 +146,11 @@ export class VideoListComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.sort = <SortField>routeParams['sort'] || '-createdDate';
|
||||
|
||||
this.pagination.currentPage = parseInt(routeParams['page']) || 1;
|
||||
if (routeParams['page'] !== undefined) {
|
||||
this.pagination.currentPage = parseInt(routeParams['page']);
|
||||
} else {
|
||||
this.pagination.currentPage = 1;
|
||||
}
|
||||
|
||||
this.changeDetector.detectChanges();
|
||||
}
|
||||
|
@ -1,16 +1,12 @@
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { ROUTER_DIRECTIVES } from '@angular/router';
|
||||
|
||||
import { SortField, Video, VideoService } from '../shared';
|
||||
import { User } from '../../shared';
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-miniature',
|
||||
styles: [ require('./video-miniature.component.scss') ],
|
||||
template: require('./video-miniature.component.html'),
|
||||
directives: [ ROUTER_DIRECTIVES ],
|
||||
pipes: [ DatePipe ]
|
||||
styleUrls: [ './video-miniature.component.scss' ],
|
||||
templateUrl: './video-miniature.component.html'
|
||||
})
|
||||
|
||||
export class VideoMiniatureComponent {
|
||||
@ -40,7 +36,7 @@ export class VideoMiniatureComponent {
|
||||
if (confirm('Do you really want to remove this video?')) {
|
||||
this.videoService.removeVideo(id).subscribe(
|
||||
status => this.removed.emit(true),
|
||||
error => alert(error)
|
||||
error => alert(error.text)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { SortField } from '../shared';
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-sort',
|
||||
template: require('./video-sort.component.html')
|
||||
templateUrl: './video-sort.component.html'
|
||||
})
|
||||
|
||||
export class VideoSortComponent {
|
||||
|
@ -1,18 +1,13 @@
|
||||
import { Component, ElementRef, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Component, ElementRef, NgZone, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe';
|
||||
|
||||
import { LoaderComponent, Video, VideoService } from '../shared';
|
||||
import { Video, VideoService } from '../shared';
|
||||
import { WebTorrentService } from './webtorrent.service';
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-watch',
|
||||
template: require('./video-watch.component.html'),
|
||||
styles: [ require('./video-watch.component.scss') ],
|
||||
providers: [ WebTorrentService ],
|
||||
directives: [ LoaderComponent ],
|
||||
pipes: [ BytesPipe ]
|
||||
templateUrl: './video-watch.component.html',
|
||||
styleUrls: [ './video-watch.component.scss' ]
|
||||
})
|
||||
|
||||
export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||
@ -31,6 +26,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(
|
||||
private elementRef: ElementRef,
|
||||
private ngZone: NgZone,
|
||||
private route: ActivatedRoute,
|
||||
private videoService: VideoService,
|
||||
private webTorrentService: WebTorrentService
|
||||
@ -65,12 +61,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh each second
|
||||
this.torrentInfosInterval = setInterval(() => {
|
||||
this.downloadSpeed = torrent.downloadSpeed;
|
||||
this.numPeers = torrent.numPeers;
|
||||
this.uploadSpeed = torrent.uploadSpeed;
|
||||
}, 1000);
|
||||
this.runInProgress(torrent);
|
||||
});
|
||||
}
|
||||
|
||||
@ -91,7 +82,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||
this.video = video;
|
||||
this.loadVideo();
|
||||
},
|
||||
error => alert(error)
|
||||
error => alert(error.text)
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -100,4 +91,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||
this.error = true;
|
||||
console.error('The video load seems to be abnormally long.');
|
||||
}
|
||||
|
||||
private runInProgress(torrent: any) {
|
||||
// Refresh each second
|
||||
this.torrentInfosInterval = setInterval(() => {
|
||||
this.ngZone.run(() => {
|
||||
this.downloadSpeed = torrent.downloadSpeed;
|
||||
this.numPeers = torrent.numPeers;
|
||||
this.uploadSpeed = torrent.uploadSpeed;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ROUTER_DIRECTIVES } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
template: '<router-outlet></router-outlet>',
|
||||
directives: [ ROUTER_DIRECTIVES ]
|
||||
template: '<router-outlet></router-outlet>'
|
||||
})
|
||||
|
||||
export class VideosComponent {
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { RouterConfig } from '@angular/router';
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { VideoAddComponent } from './video-add';
|
||||
import { VideoListComponent } from './video-list';
|
||||
import { VideosComponent } from './videos.component';
|
||||
import { VideoWatchComponent } from './video-watch';
|
||||
|
||||
export const VideosRoutes: RouterConfig = [
|
||||
export const VideosRoutes: Routes = [
|
||||
{
|
||||
path: 'videos',
|
||||
component: VideosComponent,
|
||||
|
116
client/src/custom-typings.d.ts
vendored
116
client/src/custom-typings.d.ts
vendored
@ -1,49 +1,93 @@
|
||||
/*
|
||||
* Custom Type Definitions
|
||||
* When including 3rd party modules you also need to include the type definition for the module
|
||||
* if they don't provide one within the module. You can try to install it with typings
|
||||
* if they don't provide one within the module. You can try to install it with @types
|
||||
|
||||
typings install node --save
|
||||
npm install @types/node
|
||||
npm install @types/lodash
|
||||
|
||||
* If you can't find the type definition in the registry we can make an ambient definition in
|
||||
* If you can't find the type definition in the registry we can make an ambient/global definition in
|
||||
* this file for now. For example
|
||||
|
||||
declare module "my-module" {
|
||||
declare module 'my-module' {
|
||||
export function doesSomething(value: string): string;
|
||||
}
|
||||
|
||||
* If you are using a CommonJS module that is using module.exports then you will have to write your
|
||||
* types using export = yourObjectOrFunction with a namespace above it
|
||||
* notice how we have to create a namespace that is equal to the function we're
|
||||
* assigning the export to
|
||||
|
||||
declare module 'jwt-decode' {
|
||||
function jwtDecode(token: string): any;
|
||||
namespace jwtDecode {}
|
||||
export = jwtDecode;
|
||||
}
|
||||
|
||||
*
|
||||
* If you're prototying and you will fix the types later you can also declare it as type any
|
||||
*
|
||||
|
||||
declare var assert: any;
|
||||
declare var _: any;
|
||||
declare var $: any;
|
||||
|
||||
*
|
||||
* If you're importing a module that uses Node.js modules which are CommonJS you need to import as
|
||||
* in the files such as main.browser.ts or any file within app/
|
||||
*
|
||||
|
||||
import * as _ from 'lodash'
|
||||
|
||||
* You can include your type definitions in this file until you create one for the typings registry
|
||||
* see https://github.com/typings/registry
|
||||
* You can include your type definitions in this file until you create one for the @types
|
||||
*
|
||||
*/
|
||||
|
||||
// support NodeJS modules without type definitions
|
||||
declare module '*';
|
||||
|
||||
// Extra variables that live on Global that will be replaced by webpack DefinePlugin
|
||||
declare var ENV: string;
|
||||
declare var HMR: boolean;
|
||||
declare var System: SystemJS;
|
||||
|
||||
interface SystemJS {
|
||||
import: (path?: string) => Promise<any>;
|
||||
}
|
||||
|
||||
interface GlobalEnvironment {
|
||||
ENV;
|
||||
HMR;
|
||||
SystemJS: SystemJS;
|
||||
System: SystemJS;
|
||||
}
|
||||
|
||||
interface Es6PromiseLoader {
|
||||
(id: string): (exportName?: string) => Promise<any>;
|
||||
}
|
||||
|
||||
type FactoryEs6PromiseLoader = () => Es6PromiseLoader;
|
||||
type FactoryPromise = () => Promise<any>;
|
||||
|
||||
type AsyncRoutes = {
|
||||
[component: string]: Es6PromiseLoader |
|
||||
Function |
|
||||
FactoryEs6PromiseLoader |
|
||||
FactoryPromise
|
||||
};
|
||||
|
||||
|
||||
type IdleCallbacks = Es6PromiseLoader |
|
||||
Function |
|
||||
FactoryEs6PromiseLoader |
|
||||
FactoryPromise ;
|
||||
|
||||
interface WebpackModule {
|
||||
hot: {
|
||||
data?: any,
|
||||
idle: any,
|
||||
accept(dependencies?: string | string[], callback?: (updatedDependencies?: any) => void): void;
|
||||
decline(dependencies?: string | string[]): void;
|
||||
decline(deps?: any | string | string[]): void;
|
||||
dispose(callback?: (data?: any) => void): void;
|
||||
addDisposeHandler(callback?: (data?: any) => void): void;
|
||||
removeDisposeHandler(callback?: (data?: any) => void): void;
|
||||
@ -54,66 +98,26 @@ interface WebpackModule {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
interface WebpackRequire {
|
||||
context(file: string, flag?: boolean, exp?: RegExp): any;
|
||||
(id: string): any;
|
||||
(paths: string[], callback: (...modules: any[]) => void): void;
|
||||
ensure(ids: string[], callback: (req: WebpackRequire) => void, chunkName?: string): void;
|
||||
context(directory: string, useSubDirectories?: boolean, regExp?: RegExp): WebpackContext;
|
||||
}
|
||||
|
||||
interface WebpackContext extends WebpackRequire {
|
||||
keys(): string[];
|
||||
}
|
||||
|
||||
interface ErrorStackTraceLimit {
|
||||
stackTraceLimit: number;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Extend typings
|
||||
interface NodeRequire extends WebpackRequire {}
|
||||
interface ErrorConstructor extends ErrorStackTraceLimit {}
|
||||
interface NodeRequireFunction extends Es6PromiseLoader {}
|
||||
interface NodeModule extends WebpackModule {}
|
||||
interface Global extends GlobalEnvironment {}
|
||||
|
||||
|
||||
declare namespace Reflect {
|
||||
function decorate(decorators: ClassDecorator[], target: Function): Function;
|
||||
function decorate(
|
||||
decorators: (PropertyDecorator | MethodDecorator)[],
|
||||
target: Object,
|
||||
targetKey: string | symbol,
|
||||
descriptor?: PropertyDescriptor): PropertyDescriptor;
|
||||
|
||||
function metadata(metadataKey: any, metadataValue: any): {
|
||||
(target: Function): void;
|
||||
(target: Object, propertyKey: string | symbol): void;
|
||||
};
|
||||
function defineMetadata(metadataKey: any, metadataValue: any, target: Object): void;
|
||||
function defineMetadata(
|
||||
metadataKey: any,
|
||||
metadataValue: any,
|
||||
target: Object,
|
||||
targetKey: string | symbol): void;
|
||||
function hasMetadata(metadataKey: any, target: Object): boolean;
|
||||
function hasMetadata(metadataKey: any, target: Object, targetKey: string | symbol): boolean;
|
||||
function hasOwnMetadata(metadataKey: any, target: Object): boolean;
|
||||
function hasOwnMetadata(metadataKey: any, target: Object, targetKey: string | symbol): boolean;
|
||||
function getMetadata(metadataKey: any, target: Object): any;
|
||||
function getMetadata(metadataKey: any, target: Object, targetKey: string | symbol): any;
|
||||
function getOwnMetadata(metadataKey: any, target: Object): any;
|
||||
function getOwnMetadata(metadataKey: any, target: Object, targetKey: string | symbol): any;
|
||||
function getMetadataKeys(target: Object): any[];
|
||||
function getMetadataKeys(target: Object, targetKey: string | symbol): any[];
|
||||
function getOwnMetadataKeys(target: Object): any[];
|
||||
function getOwnMetadataKeys(target: Object, targetKey: string | symbol): any[];
|
||||
function deleteMetadata(metadataKey: any, target: Object): boolean;
|
||||
function deleteMetadata(metadataKey: any, target: Object, targetKey: string | symbol): boolean;
|
||||
}
|
||||
|
||||
|
||||
// We need this here since there is a problem with Zone.js typings
|
||||
interface Thenable<T> {
|
||||
then<U>(
|
||||
onFulfilled?: (value: T) => U | Thenable<U>,
|
||||
onRejected?: (error: any) => U | Thenable<U>): Thenable<U>;
|
||||
then<U>(
|
||||
onFulfilled?: (value: T) => U | Thenable<U>,
|
||||
onRejected?: (error: any) => void): Thenable<U>;
|
||||
catch<U>(onRejected?: (error: any) => U | Thenable<U>): Thenable<U>;
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<base href="/">
|
||||
|
@ -1,28 +1,20 @@
|
||||
import { enableProdMode, provide } from '@angular/core';
|
||||
import {
|
||||
HTTP_PROVIDERS,
|
||||
RequestOptions,
|
||||
XHRBackend
|
||||
} from '@angular/http';
|
||||
import { bootstrap } from '@angular/platform-browser-dynamic';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
import { decorateModuleRef } from './app/environment';
|
||||
import { bootloader } from '@angularclass/hmr';
|
||||
/*
|
||||
* App Module
|
||||
* our top level module that holds all of our components
|
||||
*/
|
||||
import { AppModule } from './app';
|
||||
|
||||
import { AppComponent } from './app/app.component';
|
||||
import { routes } from './app/app.routes';
|
||||
import { AuthHttp, AuthService } from './app/shared';
|
||||
|
||||
if (process.env.ENV === 'production') {
|
||||
enableProdMode();
|
||||
/*
|
||||
* Bootstrap our Angular app with a top level NgModule
|
||||
*/
|
||||
export function main(): Promise<any> {
|
||||
return platformBrowserDynamic()
|
||||
.bootstrapModule(AppModule)
|
||||
.then(decorateModuleRef)
|
||||
.catch(err => console.error(err));
|
||||
}
|
||||
|
||||
bootstrap(AppComponent, [
|
||||
HTTP_PROVIDERS,
|
||||
provide(AuthHttp, {
|
||||
useFactory: (backend: XHRBackend, defaultOptions: RequestOptions, authService: AuthService) => {
|
||||
return new AuthHttp(backend, defaultOptions, authService);
|
||||
},
|
||||
deps: [ XHRBackend, RequestOptions, AuthService ]
|
||||
}),
|
||||
AuthService,
|
||||
provideRouter(routes)
|
||||
]);
|
||||
bootloader(main);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user