Merge branch 'master' into webseed-merged

pull/10/merge
Chocobozzz 2016-10-02 15:39:09 +02:00
commit a6375e6966
186 changed files with 5711 additions and 2542 deletions

7
.gitignore vendored
View File

@ -14,4 +14,11 @@ uploads
thumbnails thumbnails
config/production.yaml config/production.yaml
ffmpeg ffmpeg
<<<<<<< HEAD
torrents torrents
=======
.tags
*.sublime-project
*.sublime-workspace
torrents/
>>>>>>> master

View File

@ -1,8 +1,8 @@
language: node_js language: node_js
node_js: node_js:
- "4.4" - "4.5"
- "6.2" - "6.6"
env: env:
- CXX=g++-4.8 - CXX=g++-4.8
@ -19,8 +19,10 @@ sudo: false
services: services:
- mongodb - mongodb
before_install: if [[ `npm -v` != 3* ]]; then npm i -g npm@3; fi
before_script: before_script:
- npm install electron-prebuilt -g - npm install electron -g
- npm run build - npm run build
- wget --no-check-certificate "https://download.cpy.re/ffmpeg/ffmpeg-release-3.0.2-64bit-static.tar.xz" - 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 - tar xf ffmpeg-release-3.0.2-64bit-static.tar.xz

View File

@ -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 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 * 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 ## 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) - [ ] Validate the prototype (test PeerTube in a real world with many pods and videos)
- [ ] Manage API breaks - [ ] Manage API breaks
- [ ] Add "DDOS" security (check if a pod don't send too many requests for example) - [ ] Add "DDOS" security (check if a pod don't send too many requests for example)
- [ ] Admin panel - [X] Admin panel
- [ ] Stats about the network (how many friends, how many requests per hour...) - [X] Stats
- [ ] Stats about videos - [X] Friends list
- [ ] Manage users (create/remove) - [X] Manage users (create/remove)
- [ ] User playlists
- [ ] User subscriptions (by tags, author...)
## Installation ## Installation
@ -111,6 +114,7 @@ Thanks to [WebTorrent](https://github.com/feross/webtorrent), we can make P2P (t
### Dependencies ### Dependencies
* **NodeJS >= 4.2** * **NodeJS >= 4.2**
* **npm >= 3.0**
* OpenSSL (cli) * OpenSSL (cli)
* MongoDB * MongoDB
* ffmpeg xvfb-run libgtk2.0-0 libgconf-2-4 libnss3 libasound2 libxtst6 libxss1 libnotify-bin (for electron) * 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 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 # 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) #### Other distribution... (PR welcome)
@ -160,6 +165,10 @@ Finally, run the server with the `production` `NODE_ENV` variable set.
$ NODE_ENV=production npm start $ 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 ### Other commands
To print all available command run: To print all available command run:

View File

@ -8,10 +8,15 @@ function hasProcessFlag (flag) {
return process.argv.join('').indexOf(flag) > -1 return process.argv.join('').indexOf(flag) > -1
} }
function isWebpackDevServer () {
return process.argv[1] && !!(/webpack-dev-server$/.exec(process.argv[1]))
}
function root (args) { function root (args) {
args = Array.prototype.slice.call(arguments, 0) args = Array.prototype.slice.call(arguments, 0)
return path.join.apply(path, [ROOT].concat(args)) return path.join.apply(path, [ROOT].concat(args))
} }
exports.hasProcessFlag = hasProcessFlag exports.hasProcessFlag = hasProcessFlag
exports.isWebpackDevServer = isWebpackDevServer
exports.root = root exports.root = root

View File

@ -5,9 +5,11 @@ const helpers = require('./helpers')
* Webpack Plugins * 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 HtmlWebpackPlugin = require('html-webpack-plugin')
const ForkCheckerPlugin = require('awesome-typescript-loader').ForkCheckerPlugin const ForkCheckerPlugin = require('awesome-typescript-loader').ForkCheckerPlugin
const AssetsPlugin = require('assets-webpack-plugin')
const ContextReplacementPlugin = require('webpack/lib/ContextReplacementPlugin')
const WebpackNotifierPlugin = require('webpack-notifier') const WebpackNotifierPlugin = require('webpack-notifier')
/* /*
@ -15,7 +17,8 @@ const WebpackNotifierPlugin = require('webpack-notifier')
*/ */
const METADATA = { const METADATA = {
title: 'PeerTube', title: 'PeerTube',
baseUrl: '/' baseUrl: '/',
isDevServer: helpers.isWebpackDevServer()
} }
/* /*
@ -23,247 +26,241 @@ const METADATA = {
* *
* See: http://webpack.github.io/docs/configuration.html#cli * See: http://webpack.github.io/docs/configuration.html#cli
*/ */
module.exports = { module.exports = function (options) {
/* var isProd = options.env === 'production'
* Static metadata for index.html
*
* See: (custom attribute)
*/
metadata: METADATA,
/* return {
* Cache generated modules and chunks to improve performance for multiple incremental builds.
* This is enabled by default in watch mode.
* You can pass false to disable it.
*
* See: http://webpack.github.io/docs/configuration.html#cache
*/
// cache: false,
/*
* The entry point for the bundle
* Our Angular.js app
*
* See: http://webpack.github.io/docs/configuration.html#entry
*/
entry: {
'polyfills': './src/polyfills.ts',
'vendor': './src/vendor.ts',
'main': './src/main.ts'
},
/*
* Options affecting the resolving of modules.
*
* See: http://webpack.github.io/docs/configuration.html#resolve
*/
resolve: {
/* /*
* An array of extensions that should be used to resolve modules. * Static metadata for index.html
* *
* See: http://webpack.github.io/docs/configuration.html#resolve-extensions * See: (custom attribute)
*/ */
extensions: [ '', '.ts', '.js', '.scss' ], metadata: METADATA,
// Make sure root is src
root: helpers.root('src'),
// remove other default values
modulesDirectories: [ 'node_modules' ],
packageAlias: 'browser'
},
output: {
publicPath: '/client/'
},
/*
* Options affecting the normal modules.
*
* See: http://webpack.github.io/docs/configuration.html#module
*/
module: {
/* /*
* An array of applied pre and post loaders. * Cache generated modules and chunks to improve performance for multiple incremental builds.
* This is enabled by default in watch mode.
* You can pass false to disable it.
* *
* See: http://webpack.github.io/docs/configuration.html#module-preloaders-module-postloaders * See: http://webpack.github.io/docs/configuration.html#cache
*/ */
preLoaders: [ // cache: false,
/*
* The entry point for the bundle
* Our Angular.js app
*
* See: http://webpack.github.io/docs/configuration.html#entry
*/
entry: {
'polyfills': './src/polyfills.ts',
'vendor': './src/vendor.ts',
'main': './src/main.ts'
},
/*
* Options affecting the resolving of modules.
*
* See: http://webpack.github.io/docs/configuration.html#resolve
*/
resolve: {
/*
* An array of extensions that should be used to resolve modules.
*
* See: http://webpack.github.io/docs/configuration.html#resolve-extensions
*/
extensions: [ '', '.ts', '.js', '.scss' ],
// Make sure root is src
root: helpers.root('src'),
// remove other default values
modulesDirectories: [ 'node_modules' ]
},
output: {
publicPath: '/client/'
},
/*
* Options affecting the normal modules.
*
* See: http://webpack.github.io/docs/configuration.html#module
*/
module: {
/*
* An array of applied pre and post loaders.
*
* See: http://webpack.github.io/docs/configuration.html#module-preloaders-module-postloaders
*/
preLoaders: [
{
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')]
}
],
/* /*
* Tslint loader support for *.ts files * An array of automatically applied loaders.
* *
* See: https://github.com/wbuchwalter/tslint-loader * IMPORTANT: The loaders here are resolved relative to the resource which they are applied to.
* This means they are not resolved relative to the configuration file.
*
* See: http://webpack.github.io/docs/configuration.html#module-loaders
*/ */
// { test: /\.ts$/, loader: 'tslint-loader', exclude: [ helpers.root('node_modules') ] }, loaders: [
/*
* Typescript loader support for .ts and Angular 2 async routes via .async.ts
*
* See: https://github.com/s-panferov/awesome-typescript-loader
*/
{
test: /\.ts$/,
loaders: [
'@angularclass/hmr-loader?pretty=' + !isProd + '&prod=' + isProd,
'awesome-typescript-loader',
'angular2-template-loader'
],
exclude: [/\.(spec|e2e)\.ts$/]
},
/*
* Json loader support for *.json files.
*
* See: https://github.com/webpack/json-loader
*/
{
test: /\.json$/,
loader: 'json-loader'
},
{
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
*
* See: https://github.com/webpack/raw-loader
*/
{
test: /\.html$/,
loader: 'raw-loader',
exclude: [ helpers.root('src/index.html') ]
}
]
},
sassLoader: {
precision: 10
},
/*
* Add additional plugins to the compiler.
*
* See: http://webpack.github.io/docs/configuration.html#plugins
*/
plugins: [
new AssetsPlugin({
path: helpers.root('dist'),
filename: 'webpack-assets.json',
prettyPrint: true
}),
/* /*
* Source map loader support for *.js files * Plugin: ForkCheckerPlugin
* Extracts SourceMaps for source files that as added as sourceMappingURL comment. * Description: Do type checking in a separate process, so webpack don't need to wait.
* *
* See: https://github.com/webpack/source-map-loader * See: https://github.com/s-panferov/awesome-typescript-loader#forkchecker-boolean-defaultfalse
*/ */
{ new ForkCheckerPlugin(),
test: /\.js$/,
loader: 'source-map-loader',
exclude: [
// these packages have problems with their sourcemaps
helpers.root('node_modules/rxjs'),
helpers.root('node_modules/@angular')
]
}
/*
* Plugin: CommonsChunkPlugin
* Description: Shares common code between the pages.
* It identifies common modules and put them into a commons chunk.
*
* See: https://webpack.github.io/docs/list-of-plugins.html#commonschunkplugin
* See: https://github.com/webpack/docs/wiki/optimization#multi-page-app
*/
new webpack.optimize.CommonsChunkPlugin({
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.
*
* Copies project static assets.
*
* See: https://www.npmjs.com/package/copy-webpack-plugin
*/
new CopyWebpackPlugin([
{
from: 'src/assets',
to: 'assets'
},
{
from: 'node_modules/webtorrent/webtorrent.min.js',
to: 'assets/webtorrent'
}
]),
/*
* Plugin: HtmlWebpackPlugin
* Description: Simplifies creation of HTML files to serve your webpack bundles.
* This is especially useful for webpack bundles that include a hash in the filename
* which changes every compilation.
*
* See: https://github.com/ampedandwired/html-webpack-plugin
*/
new HtmlWebpackPlugin({
template: 'src/index.html',
chunksSortMode: 'dependency'
}),
new WebpackNotifierPlugin({ alwaysNotify: true })
], ],
/* /*
* An array of automatically applied loaders. * Include polyfills or mocks for various node stuff
* Description: Node configuration
* *
* IMPORTANT: The loaders here are resolved relative to the resource which they are applied to. * See: https://webpack.github.io/docs/configuration.html#node
* This means they are not resolved relative to the configuration file.
*
* See: http://webpack.github.io/docs/configuration.html#module-loaders
*/ */
loaders: [ node: {
global: 'window',
/* crypto: 'empty',
* Typescript loader support for .ts and Angular 2 async routes via .async.ts fs: 'empty',
* events: true,
* See: https://github.com/s-panferov/awesome-typescript-loader module: false,
*/ clearImmediate: false,
{ setImmediate: false
test: /\.ts$/, }
loader: 'awesome-typescript-loader',
exclude: [/\.(spec|e2e)\.ts$/]
},
/*
* Json loader support for *.json files.
*
* See: https://github.com/webpack/json-loader
*/
{
test: /\.json$/,
loader: 'json-loader'
},
{
test: /\.scss$/,
exclude: /node_modules/,
loaders: [ 'raw-loader', 'sass-loader' ]
},
{
test: /\.(woff2?|ttf|eot|svg)$/,
loader: 'url?limit=10000&name=assets/fonts/[hash].[ext]'
},
/* Raw loader support for *.html
* Returns file content as string
*
* See: https://github.com/webpack/raw-loader
*/
{
test: /\.html$/,
loader: 'raw-loader',
exclude: [ helpers.root('src/index.html') ]
}
]
},
sassLoader: {
precision: 10
},
/*
* Add additional plugins to the compiler.
*
* See: http://webpack.github.io/docs/configuration.html#plugins
*/
plugins: [
/*
* Plugin: ForkCheckerPlugin
* Description: Do type checking in a separate process, so webpack don't need to wait.
*
* See: https://github.com/s-panferov/awesome-typescript-loader#forkchecker-boolean-defaultfalse
*/
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.
* It identifies common modules and put them into a commons chunk.
*
* See: https://webpack.github.io/docs/list-of-plugins.html#commonschunkplugin
* See: https://github.com/webpack/docs/wiki/optimization#multi-page-app
*/
new webpack.optimize.CommonsChunkPlugin({
name: [ 'polyfills', 'vendor' ].reverse()
}),
/*
* Plugin: CopyWebpackPlugin
* Description: Copy files and directories in webpack.
*
* Copies project static assets.
*
* See: https://www.npmjs.com/package/copy-webpack-plugin
*/
new CopyWebpackPlugin([
{
from: 'src/assets',
to: 'assets'
},
{
from: 'node_modules/webtorrent/webtorrent.min.js',
to: 'assets/webtorrent'
}
]),
/*
* Plugin: HtmlWebpackPlugin
* Description: Simplifies creation of HTML files to serve your webpack bundles.
* This is especially useful for webpack bundles that include a hash in the filename
* which changes every compilation.
*
* See: https://github.com/ampedandwired/html-webpack-plugin
*/
new HtmlWebpackPlugin({
template: 'src/index.html',
chunksSortMode: 'dependency'
}),
new WebpackNotifierPlugin({ alwaysNotify: true })
],
/*
* Include polyfills or mocks for various node stuff
* Description: Node configuration
*
* See: https://webpack.github.io/docs/configuration.html#node
*/
node: {
global: 'window',
crypto: 'empty',
fs: 'empty',
events: true,
module: false,
clearImmediate: false,
setImmediate: false
} }
} }

View File

@ -6,15 +6,18 @@ const commonConfig = require('./webpack.common.js') // the settings that are com
* Webpack Plugins * Webpack Plugins
*/ */
const DefinePlugin = require('webpack/lib/DefinePlugin') const DefinePlugin = require('webpack/lib/DefinePlugin')
const NamedModulesPlugin = require('webpack/lib/NamedModulesPlugin')
/** /**
* Webpack Constants * Webpack Constants
*/ */
const ENV = process.env.ENV = process.env.NODE_ENV = 'development' 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 HMR = helpers.hasProcessFlag('hot')
const METADATA = webpackMerge(commonConfig.metadata, { const METADATA = webpackMerge(commonConfig({env: ENV}).metadata, {
host: 'localhost', host: HOST,
port: 3000, port: PORT,
ENV: ENV, ENV: ENV,
HMR: HMR HMR: HMR
}) })
@ -24,119 +27,136 @@ const METADATA = webpackMerge(commonConfig.metadata, {
* *
* See: http://webpack.github.io/docs/configuration.html#cli * 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
*
* See: (custom attribute)
*/
metadata: METADATA,
/**
* Switch loaders to debug mode.
*
* See: http://webpack.github.io/docs/configuration.html#debug
*/
debug: true,
/**
* Developer tool to enhance debugging
*
* See: http://webpack.github.io/docs/configuration.html#devtool
* See: https://github.com/webpack/docs/wiki/build-performance#sourcemaps
*/
devtool: 'cheap-module-source-map',
/**
* Options affecting the output of the compilation.
*
* See: http://webpack.github.io/docs/configuration.html#output
*/
output: {
/** /**
* The output directory as absolute path (required). * Merged metadata from webpack.common.js for index.html
* *
* See: http://webpack.github.io/docs/configuration.html#output-path * See: (custom attribute)
*/ */
path: helpers.root('dist'), metadata: METADATA,
/** /**
* Specifies the name of each output file on disk. * Switch loaders to debug mode.
* IMPORTANT: You must not specify an absolute path here!
* *
* See: http://webpack.github.io/docs/configuration.html#output-filename * See: http://webpack.github.io/docs/configuration.html#debug
*/ */
filename: '[name].bundle.js', debug: true,
/** /**
* The filename of the SourceMaps for the JavaScript files. * Developer tool to enhance debugging
* They are inside the output.path directory.
* *
* See: http://webpack.github.io/docs/configuration.html#output-sourcemapfilename * See: http://webpack.github.io/docs/configuration.html#devtool
* See: https://github.com/webpack/docs/wiki/build-performance#sourcemaps
*/ */
sourceMapFilename: '[name].map', devtool: 'cheap-module-source-map',
/** The filename of non-entry chunks as relative path
* inside the output.path directory.
*
* See: http://webpack.github.io/docs/configuration.html#output-chunkfilename
*/
chunkFilename: '[id].chunk.js'
},
externals: {
webtorrent: 'WebTorrent'
},
plugins: [
/** /**
* Plugin: DefinePlugin * Options affecting the output of the compilation.
* Description: Define free variables.
* Useful for having development builds with debug logging or adding global constants.
* *
* Environment helpers * See: http://webpack.github.io/docs/configuration.html#output
*
* See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
*/ */
// NOTE: when adding more properties, make sure you include them in custom-typings.d.ts output: {
new DefinePlugin({ /**
'ENV': JSON.stringify(METADATA.ENV), * The output directory as absolute path (required).
'HMR': METADATA.HMR, *
'process.env': { * See: http://webpack.github.io/docs/configuration.html#output-path
*/
path: helpers.root('dist'),
/**
* Specifies the name of each output file on disk.
* IMPORTANT: You must not specify an absolute path here!
*
* See: http://webpack.github.io/docs/configuration.html#output-filename
*/
filename: '[name].bundle.js',
/**
* The filename of the SourceMaps for the JavaScript files.
* They are inside the output.path directory.
*
* See: http://webpack.github.io/docs/configuration.html#output-sourcemapfilename
*/
sourceMapFilename: '[name].map',
/** The filename of non-entry chunks as relative path
* inside the output.path directory.
*
* See: http://webpack.github.io/docs/configuration.html#output-chunkfilename
*/
chunkFilename: '[id].chunk.js',
library: 'ac_[name]',
libraryTarget: 'var'
},
externals: {
webtorrent: 'WebTorrent'
},
plugins: [
/**
* Plugin: DefinePlugin
* Description: Define free variables.
* Useful for having development builds with debug logging or adding global constants.
*
* Environment helpers
*
* See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
*/
// NOTE: when adding more properties, make sure you include them in custom-typings.d.ts
new DefinePlugin({
'ENV': JSON.stringify(METADATA.ENV), 'ENV': JSON.stringify(METADATA.ENV),
'NODE_ENV': JSON.stringify(METADATA.ENV), 'HMR': METADATA.HMR,
'HMR': METADATA.HMR 'process.env': {
} 'ENV': JSON.stringify(METADATA.ENV),
}) 'NODE_ENV': JSON.stringify(METADATA.ENV),
], 'HMR': METADATA.HMR
}
}),
/** new NamedModulesPlugin()
* Static analysis linter for TypeScript advanced options configuration ],
* Description: An extensible linter for the TypeScript language.
*
* See: https://github.com/wbuchwalter/tslint-loader
*/
tslint: {
emitErrors: false,
failOnHint: false,
resourcePath: 'src'
},
/* /**
* Include polyfills or mocks for various node stuff * Static analysis linter for TypeScript advanced options configuration
* Description: Node configuration * Description: An extensible linter for the TypeScript language.
* *
* See: https://webpack.github.io/docs/configuration.html#node * See: https://github.com/wbuchwalter/tslint-loader
*/ */
node: { tslint: {
global: 'window', emitErrors: false,
crypto: 'empty', failOnHint: false,
process: true, resourcePath: 'src'
module: false, },
clearImmediate: false,
setImmediate: false
}
}) 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
*
* See: https://webpack.github.io/docs/configuration.html#node
*/
node: {
global: 'window',
crypto: 'empty',
process: true,
module: false,
clearImmediate: false,
setImmediate: false
}
})
}

View File

@ -9,10 +9,12 @@ const commonConfig = require('./webpack.common.js') // the settings that are com
/** /**
* Webpack Plugins * Webpack Plugins
*/ */
// const ProvidePlugin = require('webpack/lib/ProvidePlugin')
const DefinePlugin = require('webpack/lib/DefinePlugin') 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 UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin')
const CompressionPlugin = require('compression-webpack-plugin')
const WebpackMd5Hash = require('webpack-md5-hash') const WebpackMd5Hash = require('webpack-md5-hash')
/** /**
@ -21,211 +23,210 @@ const WebpackMd5Hash = require('webpack-md5-hash')
const ENV = process.env.NODE_ENV = process.env.ENV = 'production' const ENV = process.env.NODE_ENV = process.env.ENV = 'production'
const HOST = process.env.HOST || 'localhost' const HOST = process.env.HOST || 'localhost'
const PORT = process.env.PORT || 8080 const PORT = process.env.PORT || 8080
const METADATA = webpackMerge(commonConfig.metadata, { const METADATA = webpackMerge(commonConfig({env: ENV}).metadata, {
host: HOST, host: HOST,
port: PORT, port: PORT,
ENV: ENV, ENV: ENV,
HMR: false HMR: false
}) })
module.exports = webpackMerge(commonConfig, { module.exports = function (env) {
/** return webpackMerge(commonConfig({env: ENV}), {
* Switch loaders to debug mode.
*
* See: http://webpack.github.io/docs/configuration.html#debug
*/
debug: false,
/**
* Developer tool to enhance debugging
*
* See: http://webpack.github.io/docs/configuration.html#devtool
* See: https://github.com/webpack/docs/wiki/build-performance#sourcemaps
*/
devtool: 'source-map',
/**
* Options affecting the output of the compilation.
*
* See: http://webpack.github.io/docs/configuration.html#output
*/
output: {
/** /**
* The output directory as absolute path (required). * Switch loaders to debug mode.
* *
* See: http://webpack.github.io/docs/configuration.html#output-path * See: http://webpack.github.io/docs/configuration.html#debug
*/ */
path: helpers.root('dist'), debug: false,
/** /**
* Specifies the name of each output file on disk. * Developer tool to enhance debugging
* IMPORTANT: You must not specify an absolute path here!
* *
* See: http://webpack.github.io/docs/configuration.html#output-filename * See: http://webpack.github.io/docs/configuration.html#devtool
* See: https://github.com/webpack/docs/wiki/build-performance#sourcemaps
*/ */
filename: '[name].[chunkhash].bundle.js', devtool: 'source-map',
/** /**
* The filename of the SourceMaps for the JavaScript files. * Options affecting the output of the compilation.
* They are inside the output.path directory.
* *
* See: http://webpack.github.io/docs/configuration.html#output-sourcemapfilename * See: http://webpack.github.io/docs/configuration.html#output
*/ */
sourceMapFilename: '[name].[chunkhash].bundle.map', output: {
/**
* The output directory as absolute path (required).
*
* See: http://webpack.github.io/docs/configuration.html#output-path
*/
path: helpers.root('dist'),
/**
* Specifies the name of each output file on disk.
* IMPORTANT: You must not specify an absolute path here!
*
* See: http://webpack.github.io/docs/configuration.html#output-filename
*/
filename: '[name].[chunkhash].bundle.js',
/**
* The filename of the SourceMaps for the JavaScript files.
* They are inside the output.path directory.
*
* See: http://webpack.github.io/docs/configuration.html#output-sourcemapfilename
*/
sourceMapFilename: '[name].[chunkhash].bundle.map',
/**
* The filename of non-entry chunks as relative path
* inside the output.path directory.
*
* See: http://webpack.github.io/docs/configuration.html#output-chunkfilename
*/
chunkFilename: '[id].[chunkhash].chunk.js'
},
externals: {
webtorrent: 'WebTorrent'
},
/** /**
* The filename of non-entry chunks as relative path * Add additional plugins to the compiler.
* inside the output.path directory.
* *
* See: http://webpack.github.io/docs/configuration.html#output-chunkfilename * See: http://webpack.github.io/docs/configuration.html#plugins
*/ */
chunkFilename: '[id].[chunkhash].chunk.js' plugins: [
}, /**
* Plugin: WebpackMd5Hash
* Description: Plugin to replace a standard webpack chunkhash with md5.
*
* See: https://www.npmjs.com/package/webpack-md5-hash
*/
new WebpackMd5Hash(),
externals: { /**
webtorrent: 'WebTorrent' * Plugin: DedupePlugin
}, * Description: Prevents the inclusion of duplicate code into your bundle
* and instead applies a copy of the function at runtime.
*
* See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
* See: https://github.com/webpack/docs/wiki/optimization#deduplication
*/
// new DedupePlugin(),
/** /**
* Add additional plugins to the compiler. * Plugin: DefinePlugin
* * Description: Define free variables.
* See: http://webpack.github.io/docs/configuration.html#plugins * Useful for having development builds with debug logging or adding global constants.
*/ *
plugins: [ * Environment helpers
*
/** * See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
* Plugin: WebpackMd5Hash */
* Description: Plugin to replace a standard webpack chunkhash with md5. // NOTE: when adding more properties make sure you include them in custom-typings.d.ts
* new DefinePlugin({
* See: https://www.npmjs.com/package/webpack-md5-hash
*/
new WebpackMd5Hash(),
/**
* Plugin: DedupePlugin
* Description: Prevents the inclusion of duplicate code into your bundle
* and instead applies a copy of the function at runtime.
*
* See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
* See: https://github.com/webpack/docs/wiki/optimization#deduplication
*/
new DedupePlugin(),
/**
* Plugin: DefinePlugin
* Description: Define free variables.
* Useful for having development builds with debug logging or adding global constants.
*
* Environment helpers
*
* See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
*/
// NOTE: when adding more properties make sure you include them in custom-typings.d.ts
new DefinePlugin({
'ENV': JSON.stringify(METADATA.ENV),
'HMR': METADATA.HMR,
'process.env': {
'ENV': JSON.stringify(METADATA.ENV), 'ENV': JSON.stringify(METADATA.ENV),
'NODE_ENV': JSON.stringify(METADATA.ENV), 'HMR': METADATA.HMR,
'HMR': METADATA.HMR 'process.env': {
} 'ENV': JSON.stringify(METADATA.ENV),
}), 'NODE_ENV': JSON.stringify(METADATA.ENV),
'HMR': METADATA.HMR
}
}),
/** /**
* Plugin: UglifyJsPlugin * Plugin: UglifyJsPlugin
* Description: Minimize all JavaScript output of chunks. * Description: Minimize all JavaScript output of chunks.
* Loaders are switched into minimizing mode. * Loaders are switched into minimizing mode.
* *
* See: https://webpack.github.io/docs/list-of-plugins.html#uglifyjsplugin * See: https://webpack.github.io/docs/list-of-plugins.html#uglifyjsplugin
*/ */
// NOTE: To debug prod builds uncomment //debug lines and comment //prod lines // NOTE: To debug prod builds uncomment //debug lines and comment //prod lines
new UglifyJsPlugin({ new UglifyJsPlugin({
// beautify: true, //debug // beautify: true, //debug
// mangle: false, //debug // mangle: false, //debug
// dead_code: false, //debug // dead_code: false, //debug
// unused: false, //debug // unused: false, //debug
// deadCode: false, //debug // deadCode: false, //debug
// compress: { // compress: {
// screw_ie8: true, // screw_ie8: true,
// keep_fnames: true, // keep_fnames: true,
// drop_debugger: false, // drop_debugger: false,
// dead_code: false, // dead_code: false,
// unused: false // unused: false
// }, // debug // }, // debug
// comments: true, //debug // comments: true, //debug
beautify: false, // prod beautify: false, // prod
mangle: { screw_ie8: true, keep_fnames: true }, // prod
compress: { screw_ie8: true }, // prod
comments: false // prod
}),
mangle: { new NormalModuleReplacementPlugin(
screw_ie8: true, /angular2-hmr/,
keep_fnames: true helpers.root('config/modules/angular2-hmr-prod.js')
}, // prod )
compress: { /**
screw_ie8: true * Plugin: CompressionPlugin
}, // prod * Description: Prepares compressed versions of assets to serve
* them with Content-Encoding
*
* See: https://github.com/webpack/compression-webpack-plugin
*/
// new CompressionPlugin({
// regExp: /\.css$|\.html$|\.js$|\.map$/,
// threshold: 2 * 1024
// })
comments: false // prod
}),
/**
* Plugin: CompressionPlugin
* Description: Prepares compressed versions of assets to serve
* them with Content-Encoding
*
* See: https://github.com/webpack/compression-webpack-plugin
*/
new CompressionPlugin({
regExp: /\.css$|\.html$|\.js$|\.map$/,
threshold: 2 * 1024
})
],
/**
* Static analysis linter for TypeScript advanced options configuration
* Description: An extensible linter for the TypeScript language.
*
* See: https://github.com/wbuchwalter/tslint-loader
*/
tslint: {
emitErrors: true,
failOnHint: true,
resourcePath: 'src'
},
/**
* Html loader advanced options
*
* See: https://github.com/webpack/html-loader#advanced-options
*/
// TODO: Need to workaround Angular 2's html syntax => #id [bind] (event) *ngFor
htmlLoader: {
minimize: true,
removeAttributeQuotes: false,
caseSensitive: true,
customAttrSurround: [
[/#/, /(?:)/],
[/\*/, /(?:)/],
[/\[?\(?/, /(?:)/]
], ],
customAttrAssign: [/\)?\]?=/]
},
/* /**
* Include polyfills or mocks for various node stuff * Static analysis linter for TypeScript advanced options configuration
* Description: Node configuration * Description: An extensible linter for the TypeScript language.
* *
* See: https://webpack.github.io/docs/configuration.html#node * See: https://github.com/wbuchwalter/tslint-loader
*/ */
node: { tslint: {
global: 'window', emitErrors: true,
crypto: 'empty', failOnHint: true,
process: false, resourcePath: 'src'
module: false, },
clearImmediate: false,
setImmediate: false
}
}) /**
* Html loader advanced options
*
* See: https://github.com/webpack/html-loader#advanced-options
*/
// TODO: Need to workaround Angular 2's html syntax => #id [bind] (event) *ngFor
htmlLoader: {
minimize: true,
removeAttributeQuotes: false,
caseSensitive: true,
customAttrSurround: [
[/#/, /(?:)/],
[/\*/, /(?:)/],
[/\[?\(?/, /(?:)/]
],
customAttrAssign: [/\)?\]?=/]
},
/*
* Include polyfills or mocks for various node stuff
* Description: Node configuration
*
* See: https://webpack.github.io/docs/configuration.html#node
*/
node: {
global: 'window',
crypto: 'empty',
process: false,
module: false,
clearImmediate: false,
setImmediate: false
}
})
}

View File

@ -13,61 +13,72 @@
"url": "git://github.com/Chocobozzz/PeerTube.git" "url": "git://github.com/Chocobozzz/PeerTube.git"
}, },
"scripts": { "scripts": {
"postinstall": "typings install",
"test": "standard && tslint -c ./tslint.json src/**/*.ts", "test": "standard && tslint -c ./tslint.json src/**/*.ts",
"webpack": "webpack" "webpack": "webpack"
}, },
"license": "GPLv3", "license": "GPLv3",
"dependencies": { "dependencies": {
"@angular/common": "2.0.0-rc.4", "@angular/common": "^2.0.0",
"@angular/compiler": "2.0.0-rc.4", "@angular/compiler": "^2.0.0",
"@angular/core": "2.0.0-rc.4", "@angular/core": "^2.0.0",
"@angular/http": "2.0.0-rc.4", "@angular/forms": "^2.0.0",
"@angular/platform-browser": "2.0.0-rc.4", "@angular/http": "^2.0.0",
"@angular/platform-browser-dynamic": "2.0.0-rc.4", "@angular/platform-browser": "^2.0.0",
"@angular/router": "3.0.0-beta.2", "@angular/platform-browser-dynamic": "^2.0.0",
"angular-pipes": "^2.0.0", "@angular/router": "^3.0.0",
"awesome-typescript-loader": "^0.17.0", "@angularclass/hmr": "^1.2.0",
"bootstrap-loader": "^1.0.8", "@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", "bootstrap-sass": "^3.3.6",
"compression-webpack-plugin": "^0.3.1", "compression-webpack-plugin": "^0.3.1",
"copy-webpack-plugin": "^3.0.1", "copy-webpack-plugin": "^3.0.1",
"core-js": "^2.4.0", "core-js": "^2.4.1",
"css-loader": "^0.23.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": "^3.0.2",
"es6-promise-loader": "^1.0.1", "es6-promise-loader": "^1.0.1",
"es6-shim": "^0.35.0", "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", "html-webpack-plugin": "^2.19.0",
"ie-shim": "^0.1.0", "ie-shim": "^0.1.0",
"intl": "^1.2.4", "intl": "^1.2.4",
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"ng2-bootstrap": "1.0.16", "ng2-bootstrap": "^1.1.5",
"ng2-file-upload": "^1.0.3", "ng2-file-upload": "^1.0.3",
"node-sass": "^3.7.0", "node-sass": "^3.10.0",
"normalize.css": "^4.1.1", "normalize.css": "^4.1.1",
"raw-loader": "^0.5.1", "raw-loader": "^0.5.1",
"reflect-metadata": "0.1.3", "reflect-metadata": "0.1.3",
"resolve-url-loader": "^1.4.3", "resolve-url-loader": "^1.6.0",
"rxjs": "5.0.0-beta.6", "rxjs": "5.0.0-beta.12",
"sass-loader": "^3.2.0", "sass-loader": "^4.0.2",
"source-map-loader": "^0.1.5", "source-map-loader": "^0.1.5",
"string-replace-loader": "^1.0.3",
"style-loader": "^0.13.1", "style-loader": "^0.13.1",
"ts-helpers": "^1.1.1", "ts-helpers": "^1.1.1",
"tslint": "^3.7.4", "tslint": "3.15.1",
"tslint-loader": "^2.1.4", "tslint-loader": "^2.1.4",
"typescript": "^1.8.10", "typescript": "^2.0.0",
"typings": "^1.0.4",
"url-loader": "^0.5.7", "url-loader": "^0.5.7",
"webpack": "^1.13.1", "webpack": "2.1.0-beta.22",
"webpack-md5-hash": "0.0.5", "webpack-md5-hash": "0.0.5",
"webpack-merge": "^0.13.0", "webpack-merge": "^0.14.1",
"webpack-notifier": "^1.3.0", "webpack-notifier": "^1.3.0",
"webtorrent": "^0.95.2", "webtorrent": "^0.96.0",
"zone.js": "0.6.12" "zone.js": "0.6.23"
}, },
"devDependencies": { "devDependencies": {
"codelyzer": "0.0.19", "codelyzer": "0.0.28",
"standard": "^7.0.1" "standard": "^8.0.0"
} }
} }

View 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>

View 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
);
}
}

View File

@ -0,0 +1,5 @@
import { AccountComponent } from './account.component';
export const AccountRoutes = [
{ path: 'account', component: AccountComponent }
];

View 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));
}
}

View File

@ -0,0 +1,3 @@
export * from './account.component';
export * from './account.routes';
export * from './account.service';

View File

@ -0,0 +1,8 @@
import { Component } from '@angular/core';
@Component({
template: '<router-outlet></router-outlet>'
})
export class AdminComponent {
}

View 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
]
}
];

View File

@ -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>

View File

@ -0,0 +1,7 @@
table {
margin-bottom: 40px;
}
.input-group-btn button {
width: 35px;
}

View 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));
}
}

View File

@ -0,0 +1 @@
export * from './friend-add.component';

View File

@ -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>

View File

@ -0,0 +1,3 @@
table {
margin-bottom: 40px;
}

View File

@ -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)
);
}
}

View File

@ -0,0 +1 @@
export * from './friend-list.component';

View File

@ -0,0 +1,8 @@
import { Component } from '@angular/core';
@Component({
template: '<router-outlet></router-outlet>'
})
export class FriendsComponent {
}

View 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
}
]
}
];

View File

@ -0,0 +1,5 @@
export * from './friend-add';
export * from './friend-list';
export * from './shared';
export * from './friends.component';
export * from './friends.routes';

View File

@ -0,0 +1,6 @@
export interface Friend {
id: string;
url: string;
score: number;
createdDate: Date;
}

View 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));
}
}

View File

@ -1 +1,2 @@
export * from './friend.model';
export * from './friend.service'; export * from './friend.service';

View 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';

View 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>

View File

@ -0,0 +1,7 @@
import { Component } from '@angular/core';
@Component({
selector: 'my-menu-admin',
templateUrl: './menu-admin.component.html'
})
export class MenuAdminComponent { }

View File

@ -0,0 +1,4 @@
export * from './request-stats';
export * from './shared';
export * from './requests.component';
export * from './requests.routes';

View File

@ -0,0 +1 @@
export * from './request-stats.component';

View File

@ -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>

View File

@ -0,0 +1,6 @@
.label-description {
display: inline-block;
width: 350px;
font-weight: bold;
color: black;
}

View File

@ -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);
}
}

View File

@ -0,0 +1,8 @@
import { Component } from '@angular/core';
@Component({
template: '<router-outlet></router-outlet>'
})
export class RequestsComponent {
}

View 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
}
]
}
];

View File

@ -0,0 +1,2 @@
export * from './request-stats.model';
export * from './request.service';

View 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);
}
}

View 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));
}
}

View File

@ -0,0 +1,5 @@
export * from './shared';
export * from './user-add';
export * from './user-list';
export * from './users.component';
export * from './users.routes';

View File

@ -0,0 +1 @@
export * from './user.service';

View 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 };
}
}

View File

@ -0,0 +1 @@
export * from './user-add.component';

View 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>

View 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
);
}
}

View File

@ -0,0 +1 @@
export * from './user-list.component';

View File

@ -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>

View File

@ -0,0 +1,7 @@
.glyphicon-remove {
cursor: pointer;
}
.add-user {
margin-top: 10px;
}

View 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)
);
}
}
}

View File

@ -0,0 +1,8 @@
import { Component } from '@angular/core';
@Component({
template: '<router-outlet></router-outlet>'
})
export class UsersComponent {
}

View 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
}
]
}
];

View File

@ -14,48 +14,14 @@
<div class="row"> <div class="row">
<my-menu *ngIf="isInAdmin() === false"></my-menu>
<menu class="col-md-2 col-sm-3 col-xs-3"> <my-menu-admin *ngIf="isInAdmin() === true"></my-menu-admin>
<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>
<div class="col-md-9 col-sm-8 col-xs-8 router-outlet-container"> <div class="col-md-9 col-sm-8 col-xs-8 router-outlet-container">
<router-outlet></router-outlet> <router-outlet></router-outlet>
</div> </div>
</div> </div>
<footer> <footer>
PeerTube, CopyLeft 2015-2016 PeerTube, CopyLeft 2015-2016
</footer> </footer>

View File

@ -12,40 +12,6 @@ header div {
margin-bottom: 30px; 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 { .router-outlet-container {
@media screen and (max-width: 400px) { @media screen and (max-width: 400px) {
padding: 0 3px 0 3px; padding: 0 3px 0 3px;

View File

@ -1,73 +1,16 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { ActivatedRoute, Router, ROUTER_DIRECTIVES } from '@angular/router'; import { Router } from '@angular/router';
import { FriendService } from './friends';
import {
AuthService,
AuthStatus,
SearchComponent,
SearchService
} from './shared';
import { VideoService } from './videos';
@Component({ @Component({
selector: 'my-app', selector: 'my-app',
template: require('./app.component.html'), templateUrl: './app.component.html',
styles: [ require('./app.component.scss') ], styleUrls: [ './app.component.scss' ]
directives: [ ROUTER_DIRECTIVES, SearchComponent ],
providers: [ FriendService, VideoService, SearchService ]
}) })
export class AppComponent { export class AppComponent {
choices = []; constructor(private router: Router) {}
isLoggedIn: boolean;
constructor( isInAdmin() {
private authService: AuthService, return this.router.url.indexOf('/admin/') !== -1;
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)
);
} }
} }

View 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;
}
}

View File

@ -1,15 +1,18 @@
import { RouterConfig } from '@angular/router'; import { Routes } from '@angular/router';
import { AccountRoutes } from './account';
import { LoginRoutes } from './login'; import { LoginRoutes } from './login';
import { AdminRoutes } from './admin';
import { VideosRoutes } from './videos'; import { VideosRoutes } from './videos';
export const routes: RouterConfig = [ export const routes: Routes = [
{ {
path: '', path: '',
redirectTo: '/videos/list', redirectTo: '/videos/list',
pathMatch: 'full' pathMatch: 'full'
}, },
...AdminRoutes,
...AccountRoutes,
...LoginRoutes, ...LoginRoutes,
...VideosRoutes ...VideosRoutes
]; ];

View 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 ));
}
}

View 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
];

View File

@ -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
View File

@ -0,0 +1 @@
export * from './app.module';

View File

@ -1,17 +1,16 @@
<h3>Login</h3> <h3>Login</h3>
<div *ngIf="error" class="alert alert-danger">{{ error }}</div> <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"> <div class="form-group">
<label for="username">Username</label> <label for="username">Username</label>
<input <input
type="text" class="form-control" name="username" id="username" placeholder="Username" required type="text" class="form-control" id="username" placeholder="Username" required
ngControl="username" #username="ngForm" formControlName="username"
> >
<div [hidden]="username.valid || username.pristine" class="alert alert-danger"> <div *ngIf="formErrors.username" class="alert alert-danger">
Username is required {{ formErrors.username }}
</div> </div>
</div> </div>
@ -19,12 +18,12 @@
<label for="password">Password</label> <label for="password">Password</label>
<input <input
type="password" class="form-control" name="password" id="password" placeholder="Password" required 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"> <div *ngIf="formErrors.password" class="alert alert-danger">
Password is required {{ formErrors.password }}
</div> </div>
</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> </form>

View File

@ -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 { Router } from '@angular/router';
import { AuthService } from '../shared'; import { AuthService, FormReactive } from '../shared';
@Component({ @Component({
selector: 'my-login', 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; error: string = null;
form: FormGroup;
formErrors = {
'username': '',
'password': ''
};
validationMessages = {
'username': {
'required': 'Username is required.',
},
'password': {
'required': 'Password is required.'
}
};
constructor( constructor(
private authService: AuthService, private authService: AuthService,
private formBuilder: FormBuilder,
private router: Router private router: Router
) {} ) {
super();
}
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;
const { username, password } = this.form.value;
login(username: string, password: string) {
this.authService.login(username, password).subscribe( this.authService.login(username, password).subscribe(
result => { result => this.router.navigate(['/videos/list']),
this.error = null;
this.router.navigate(['/videos/list']);
},
error => { error => {
console.error(error); console.error(error.json);
if (error.error === 'invalid_grant') { if (error.json.error === 'invalid_grant') {
this.error = 'Credentials are invalid.'; this.error = 'Credentials are invalid.';
} else { } else {
this.error = `${error.error}: ${error.error_description}`; this.error = `${error.json.error}: ${error.json.error_description}`;
} }
} }
); );

View 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>

View 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']);
}
}

View File

@ -28,7 +28,7 @@ export class AuthHttp extends Http {
return super.request(url, options) return super.request(url, options)
.catch((err) => { .catch((err) => {
if (err.status === 401) { if (err.status === 401) {
return this.handleTokenExpired(err, url, options); return this.handleTokenExpired(url, options);
} }
return Observable.throw(err); return Observable.throw(err);
@ -49,26 +49,29 @@ export class AuthHttp extends Http {
return this.request(url, options); return this.request(url, options);
} }
post(url: string, options?: RequestOptionsArgs): Observable<Response> { post(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
if (!options) options = {}; if (!options) options = {};
options.method = RequestMethod.Post; options.method = RequestMethod.Post;
options.body = body;
return this.request(url, options); return this.request(url, options);
} }
put(url: string, options?: RequestOptionsArgs): Observable<Response> { put(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
if (!options) options = {}; if (!options) options = {};
options.method = RequestMethod.Put; options.method = RequestMethod.Put;
options.body = body;
return this.request(url, options); return this.request(url, options);
} }
private handleTokenExpired(err: Response, url: string | Request, options: RequestOptionsArgs) { private handleTokenExpired(url: string | Request, options: RequestOptionsArgs) {
return this.authService.refreshAccessToken().flatMap(() => { return this.authService.refreshAccessToken()
this.setAuthorizationHeader(options.headers); .flatMap(() => {
this.setAuthorizationHeader(options.headers);
return super.request(url, options); return super.request(url, options);
}); });
} }
private setAuthorizationHeader(headers: Headers) { private setAuthorizationHeader(headers: Headers) {

View File

@ -1,15 +1,28 @@
export class User { import { User } from '../users';
export class AuthUser extends User {
private static KEYS = { private static KEYS = {
ID: 'id',
ROLE: 'role',
USERNAME: 'username' USERNAME: 'username'
}; };
id: string;
role: string;
username: string; username: string;
tokens: Tokens; tokens: Tokens;
static load() { static load() {
const usernameLocalStorage = localStorage.getItem(this.KEYS.USERNAME); const usernameLocalStorage = localStorage.getItem(this.KEYS.USERNAME);
if (usernameLocalStorage) { 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; return null;
@ -17,12 +30,14 @@ export class User {
static flush() { static flush() {
localStorage.removeItem(this.KEYS.USERNAME); localStorage.removeItem(this.KEYS.USERNAME);
localStorage.removeItem(this.KEYS.ID);
localStorage.removeItem(this.KEYS.ROLE);
Tokens.flush(); Tokens.flush();
} }
constructor(username: string, hash_tokens: any) { constructor(userHash: { id: string, username: string, role: string }, hashTokens: any) {
this.username = username; super(userHash);
this.tokens = new Tokens(hash_tokens); this.tokens = new Tokens(hashTokens);
} }
getAccessToken() { getAccessToken() {
@ -43,12 +58,14 @@ export class User {
} }
save() { 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(); this.tokens.save();
} }
} }
// Private class used only by User // Private class only used by User
class Tokens { class Tokens {
private static KEYS = { private static KEYS = {
ACCESS_TOKEN: 'access_token', ACCESS_TOKEN: 'access_token',

View File

@ -1,32 +1,39 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Headers, Http, Response, URLSearchParams } from '@angular/http'; import { Headers, Http, Response, URLSearchParams } from '@angular/http';
import { Router } from '@angular/router';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject'; import { Subject } from 'rxjs/Subject';
import { AuthStatus } from './auth-status.model'; import { AuthStatus } from './auth-status.model';
import { User } from './user.model'; import { AuthUser } from './auth-user.model';
import { RestExtractor } from '../rest';
@Injectable() @Injectable()
export class AuthService { 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_TOKEN_URL = '/api/v1/users/token';
private static BASE_USER_INFORMATIONS_URL = '/api/v1/users/me';
loginChangedSource: Observable<AuthStatus>; loginChangedSource: Observable<AuthStatus>;
private clientId: string; private clientId: string;
private clientSecret: string; private clientSecret: string;
private loginChanged: Subject<AuthStatus>; 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.loginChanged = new Subject<AuthStatus>();
this.loginChangedSource = this.loginChanged.asObservable(); this.loginChangedSource = this.loginChanged.asObservable();
// Fetch the client_id/client_secret // Fetch the client_id/client_secret
// FIXME: save in local storage? // FIXME: save in local storage?
this.http.get(AuthService.BASE_CLIENT_URL) this.http.get(AuthService.BASE_CLIENT_URL)
.map(res => res.json()) .map(this.restExtractor.extractDataGet)
.catch(this.handleError) .catch((res) => this.restExtractor.handleError(res))
.subscribe( .subscribe(
result => { result => {
this.clientId = result.client_id; this.clientId = result.client_id;
@ -34,12 +41,15 @@ export class AuthService {
console.log('Client credentials loaded.'); console.log('Client credentials loaded.');
}, },
error => { 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 // Return null if there is nothing to load
this.user = User.load(); this.user = AuthUser.load();
} }
getRefreshToken() { getRefreshToken() {
@ -64,10 +74,16 @@ export class AuthService {
return this.user.getTokenType(); return this.user.getTokenType();
} }
getUser(): User { getUser(): AuthUser {
return this.user; return this.user;
} }
isAdmin() {
if (this.user === null) return false;
return this.user.isAdmin();
}
isLoggedIn() { isLoggedIn() {
if (this.getAccessToken()) { if (this.getAccessToken()) {
return true; return true;
@ -94,21 +110,23 @@ export class AuthService {
}; };
return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options) return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options)
.map(res => res.json()) .map(this.restExtractor.extractDataGet)
.map(res => { .map(res => {
res.username = username; res.username = username;
return res; return res;
}) })
.flatMap(res => this.fetchUserInformations(res))
.map(res => this.handleLogin(res)) .map(res => this.handleLogin(res))
.catch(this.handleError); .catch((res) => this.restExtractor.handleError(res));
} }
logout() { logout() {
// TODO: make an HTTP request to revoke the tokens // TODO: make an HTTP request to revoke the tokens
this.user = null; this.user = null;
User.flush();
this.setStatus(AuthStatus.LoggedIn); AuthUser.flush();
this.setStatus(AuthStatus.LoggedOut);
} }
refreshAccessToken() { refreshAccessToken() {
@ -131,36 +149,64 @@ export class AuthService {
}; };
return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options) return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options)
.map(res => res.json()) .map(this.restExtractor.extractDataGet)
.map(res => this.handleRefreshToken(res)) .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.'
});
}
return this.restExtractor.handleError(res);
});
} }
private setStatus(status: AuthStatus) { private fetchUserInformations (obj: any) {
this.loginChanged.next(status); // 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) { private handleLogin (obj: any) {
const id = obj.id;
const username = obj.username; const username = obj.username;
const hash_tokens = { const role = obj.role;
const hashTokens = {
access_token: obj.access_token, access_token: obj.access_token,
token_type: obj.token_type, token_type: obj.token_type,
refresh_token: obj.refresh_token refresh_token: obj.refresh_token
}; };
this.user = new User(username, hash_tokens); this.user = new AuthUser({ id, username, role }, hashTokens);
this.user.save(); this.user.save();
this.setStatus(AuthStatus.LoggedIn); this.setStatus(AuthStatus.LoggedIn);
} }
private handleError (error: Response) {
console.error(error);
return Observable.throw(error.json() || { error: 'Server error' });
}
private handleRefreshToken (obj: any) { private handleRefreshToken (obj: any) {
this.user.refreshTokens(obj.access_token, obj.refresh_token); this.user.refreshTokens(obj.access_token, obj.refresh_token);
this.user.save(); this.user.save();
} }
private setStatus(status: AuthStatus) {
this.loginChanged.next(status);
}
} }

View File

@ -1,4 +1,4 @@
export * from './auth-http.service'; export * from './auth-http.service';
export * from './auth-status.model'; export * from './auth-status.model';
export * from './auth.service'; export * from './auth.service';
export * from './user.model'; export * from './auth-user.model';

View 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] + ' ';
}
}
}
}
}

View File

@ -0,0 +1,3 @@
export * from './url.validator';
export * from './user';
export * from './video';

View 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
}
};
}

View 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.',
}
};

View 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.'
}
};

View File

@ -0,0 +1,2 @@
export * from './form-validators';
export * from './form-reactive';

View File

@ -1,2 +1,5 @@
export * from './auth'; export * from './auth';
export * from './forms';
export * from './rest';
export * from './search'; export * from './search';
export * from './users';

View File

@ -0,0 +1,3 @@
export * from './rest-extractor.service';
export * from './rest-pagination';
export * from './rest.service';

View 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);
}
}

View File

@ -1,5 +1,5 @@
export interface Pagination { export interface RestPagination {
currentPage: number; currentPage: number;
itemsPerPage: number; itemsPerPage: number;
totalItems: number; totalItems: number;
} };

View 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;
}
}

View File

@ -1,15 +1,13 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { DROPDOWN_DIRECTIVES} from 'ng2-bootstrap/components/dropdown';
import { Search } from './search.model'; import { Search } from './search.model';
import { SearchField } from './search-field.type'; import { SearchField } from './search-field.type';
import { SearchService } from './search.service'; import { SearchService } from './search.service';
@Component({ @Component({
selector: 'my-search', selector: 'my-search',
template: require('./search.component.html'), templateUrl: './search.component.html'
directives: [ DROPDOWN_DIRECTIVES ]
}) })
export class SearchComponent implements OnInit { export class SearchComponent implements OnInit {
@ -25,10 +23,10 @@ export class SearchComponent implements OnInit {
value: '' value: ''
}; };
constructor(private searchService: SearchService) {} constructor(private searchService: SearchService, private router: Router) {}
ngOnInit() { ngOnInit() {
// Subscribe is the search changed // Subscribe if the search changed
// Usually changed by videos list component // Usually changed by videos list component
this.searchService.updateSearch.subscribe( this.searchService.updateSearch.subscribe(
newSearchCriterias => { newSearchCriterias => {
@ -58,6 +56,10 @@ export class SearchComponent implements OnInit {
} }
doSearch() { doSearch() {
if (this.router.url.indexOf('/videos/list') === -1) {
this.router.navigate([ '/videos/list' ]);
}
this.searchService.searchUpdated.next(this.searchCriterias); this.searchService.searchUpdated.next(this.searchCriterias);
} }

View File

@ -1,5 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject'; import { Subject } from 'rxjs/Subject';
import { ReplaySubject } from 'rxjs/ReplaySubject';
import { Search } from './search.model'; import { Search } from './search.model';
@ -12,6 +13,6 @@ export class SearchService {
constructor() { constructor() {
this.updateSearch = new Subject<Search>(); this.updateSearch = new Subject<Search>();
this.searchUpdated = new Subject<Search>(); this.searchUpdated = new ReplaySubject<Search>(1);
} }
} }

View File

@ -0,0 +1 @@
export * from './user.model';

View 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';
}
}

View File

@ -1,5 +1,4 @@
export * from './loader'; export * from './loader';
export * from './pagination.model';
export * from './sort-field.type'; export * from './sort-field.type';
export * from './video.model'; export * from './video.model';
export * from './video.service'; export * from './video.service';

View File

@ -2,8 +2,8 @@ import { Component, Input } from '@angular/core';
@Component({ @Component({
selector: 'my-loader', selector: 'my-loader',
styles: [ require('./loader.component.scss') ], styleUrls: [ './loader.component.scss' ],
template: require('./loader.component.html') templateUrl: './loader.component.html'
}) })
export class LoaderComponent { export class LoaderComponent {

View File

@ -1,11 +1,10 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Http, Response, URLSearchParams } from '@angular/http'; import { Http } from '@angular/http';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { Pagination } from './pagination.model';
import { Search } from '../../shared'; import { Search } from '../../shared';
import { SortField } from './sort-field.type'; 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'; import { Video } from './video.model';
@Injectable() @Injectable()
@ -15,68 +14,51 @@ export class VideoService {
constructor( constructor(
private authService: AuthService, private authService: AuthService,
private authHttp: AuthHttp, 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) return this.http.get(VideoService.BASE_VIDEO_URL + id)
.map(res => <Video> res.json()) .map(this.restExtractor.extractDataGet)
.catch(this.handleError); .catch((res) => this.restExtractor.handleError(res));
} }
getVideos(pagination: Pagination, sort: SortField) { getVideos(pagination: RestPagination, sort: SortField) {
const params = this.createPaginationParams(pagination); const params = this.restService.buildRestGetParams(pagination, sort);
if (sort) params.set('sort', sort);
return this.http.get(VideoService.BASE_VIDEO_URL, { search: params }) return this.http.get(VideoService.BASE_VIDEO_URL, { search: params })
.map(res => res.json()) .map(res => res.json())
.map(this.extractVideos) .map(this.extractVideos)
.catch(this.handleError); .catch((res) => this.restExtractor.handleError(res));
} }
removeVideo(id: string) { removeVideo(id: string) {
return this.authHttp.delete(VideoService.BASE_VIDEO_URL + id) return this.authHttp.delete(VideoService.BASE_VIDEO_URL + id)
.map(res => <number> res.status) .map(this.restExtractor.extractDataBool)
.catch(this.handleError); .catch((res) => this.restExtractor.handleError(res));
} }
searchVideos(search: Search, pagination: Pagination, sort: SortField) { searchVideos(search: Search, pagination: RestPagination, sort: SortField) {
const params = this.createPaginationParams(pagination); const params = this.restService.buildRestGetParams(pagination, sort);
if (search.field) params.set('field', search.field); 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 }) 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) .map(this.extractVideos)
.catch(this.handleError); .catch((res) => this.restExtractor.handleError(res));
} }
private createPaginationParams(pagination: Pagination) { private extractVideos(result: ResultList) {
const params = new URLSearchParams(); const videosJson = result.data;
const start: number = (pagination.currentPage - 1) * pagination.itemsPerPage; const totalVideos = result.total;
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;
const videos = []; const videos = [];
for (const video_json of videos_json) { for (const videoJson of videosJson) {
videos.push(new Video(video_json)); videos.push(new Video(videoJson));
} }
return { videos, totalVideos }; return { videos, totalVideos };
} }
private handleError(error: Response) {
console.error(error);
return Observable.throw(error.json().error || 'Server error');
}
} }

View File

@ -2,31 +2,31 @@
<div *ngIf="error" class="alert alert-danger">{{ error }}</div> <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"> <div class="form-group">
<label for="name">Name</label> <label for="name">Name</label>
<input <input
type="text" class="form-control" name="name" id="name" type="text" class="form-control" id="name"
ngControl="name" #name="ngForm" [(ngModel)]="video.name" formControlName="name"
> >
<div [hidden]="name.valid || name.pristine" class="alert alert-warning"> <div *ngIf="formErrors.name" class="alert alert-danger">
A name is required and should be between 3 and 50 characters long {{ formErrors.name }}
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="tags">Tags</label> <label for="tags">Tags</label>
<input <input
type="text" class="form-control" name="tags" id="tags" type="text" class="form-control" id="currentTag"
ngControl="tags" #tags="ngForm" [disabled]="isTagsInputDisabled" (keyup)="onTagKeyPress($event)" [(ngModel)]="currentTag" formControlName="currentTag" (keyup)="onTagKeyPress($event)"
> >
<div [hidden]="tags.valid || tags.pristine" class="alert alert-warning"> <div *ngIf="formErrors.currentTag" class="alert alert-danger">
A tag should be between 2 and 10 characters (alphanumeric) long {{ formErrors.currentTag }}
</div> </div>
</div> </div>
<div class="tags"> <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 }} {{ tag }}
<span class="remove" (click)="removeTag(tag)">x</span> <span class="remove" (click)="removeTag(tag)">x</span>
</div> </div>
@ -53,12 +53,12 @@
<div class="form-group"> <div class="form-group">
<label for="description">Description</label> <label for="description">Description</label>
<textarea <textarea
name="description" id="description" class="form-control" placeholder="Description..." id="description" class="form-control" placeholder="Description..."
ngControl="description" #description="ngForm" [(ngModel)]="video.description" formControlName="description"
> >
</textarea> </textarea>
<div [hidden]="description.valid || description.pristine" class="alert alert-warning"> <div *ngIf="formErrors.description" class="alert alert-danger">
A description is required and should be between 3 and 250 characters long {{ formErrors.description }}
</div> </div>
</div> </div>
@ -69,7 +69,7 @@
<div class="form-group"> <div class="form-group">
<input <input
type="submit" value="Upload" class="btn btn-default form-control" [title]="getInvalidFieldsTitle()" 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> </div>
</form> </form>

View File

@ -1,37 +1,42 @@
import { Control, ControlGroup, Validators } from '@angular/common';
import { Component, ElementRef, OnInit } from '@angular/core'; import { Component, ElementRef, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe'; import { FileUploader } from 'ng2-file-upload/ng2-file-upload';
import { PROGRESSBAR_DIRECTIVES } from 'ng2-bootstrap/components/progressbar';
import { FileSelectDirective, FileUploader } from 'ng2-file-upload/ng2-file-upload';
import { AuthService } from '../../shared'; import { AuthService, FormReactive, VIDEO_NAME, VIDEO_DESCRIPTION, VIDEO_TAGS } from '../../shared';
@Component({ @Component({
selector: 'my-videos-add', selector: 'my-videos-add',
styles: [ require('./video-add.component.scss') ], styleUrls: [ './video-add.component.scss' ],
template: require('./video-add.component.html'), templateUrl: './video-add.component.html'
directives: [ FileSelectDirective, PROGRESSBAR_DIRECTIVES ],
pipes: [ BytesPipe ]
}) })
export class VideoAddComponent implements OnInit { export class VideoAddComponent extends FormReactive implements OnInit {
currentTag: string; // Tag the user is writing in the input tags: string[] = [];
error: string = null;
videoForm: ControlGroup;
uploader: FileUploader; uploader: FileUploader;
video = {
error: string = null;
form: FormGroup;
formErrors = {
name: '', name: '',
tags: [], description: '',
description: '' currentTag: ''
};
validationMessages = {
name: VIDEO_NAME.MESSAGES,
description: VIDEO_DESCRIPTION.MESSAGES,
currentTag: VIDEO_TAGS.MESSAGES
}; };
constructor( constructor(
private authService: AuthService, private authService: AuthService,
private elementRef: ElementRef, private elementRef: ElementRef,
private formBuilder: FormBuilder,
private router: Router private router: Router
) {} ) {
super();
}
get filename() { get filename() {
if (this.uploader.queue.length === 0) { if (this.uploader.queue.length === 0) {
@ -41,20 +46,26 @@ export class VideoAddComponent implements OnInit {
return this.uploader.queue[0].file.name; return this.uploader.queue[0].file.name;
} }
get isTagsInputDisabled () { buildForm() {
return this.video.tags.length >= 3; 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() { getInvalidFieldsTitle() {
let title = ''; let title = '';
const nameControl = this.videoForm.controls['name']; const nameControl = this.form.controls['name'];
const descriptionControl = this.videoForm.controls['description']; const descriptionControl = this.form.controls['description'];
if (!nameControl.valid) { if (!nameControl.valid) {
title += 'A name is required\n'; 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'; title += 'At least one tag is required\n';
} }
@ -70,13 +81,6 @@ export class VideoAddComponent implements OnInit {
} }
ngOnInit() { 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({ this.uploader = new FileUploader({
authToken: this.authService.getRequestHeaderValue(), authToken: this.authService.getRequestHeaderValue(),
queueLimit: 1, queueLimit: 1,
@ -85,26 +89,37 @@ export class VideoAddComponent implements OnInit {
}); });
this.uploader.onBuildItemForm = (item, form) => { this.uploader.onBuildItemForm = (item, form) => {
form.append('name', this.video.name); const name = this.form.value['name'];
form.append('description', this.video.description); const description = this.form.value['description'];
for (let i = 0; i < this.video.tags.length; i++) { form.append('name', name);
form.append(`tags[${i}]`, this.video.tags[i]); 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) { onTagKeyPress(event: KeyboardEvent) {
const currentTag = this.form.value['currentTag'];
// Enter press // Enter press
if (event.keyCode === 13) { if (event.keyCode === 13) {
// Check if the tag is valid and does not already exist // Check if the tag is valid and does not already exist
if ( if (
this.currentTag !== '' && currentTag !== '' &&
this.videoForm.controls['tags'].valid && this.form.controls['currentTag'].valid &&
this.video.tags.indexOf(this.currentTag) === -1 this.tags.indexOf(currentTag) === -1
) { ) {
this.video.tags.push(this.currentTag); this.tags.push(currentTag);
this.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) { removeTag(tag: string) {
this.video.tags.splice(this.video.tags.indexOf(tag), 1); this.tags.splice(this.tags.indexOf(tag), 1);
} }
upload() { upload() {

View File

@ -1,39 +1,30 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { AsyncPipe } from '@angular/common'; import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute, Router, ROUTER_DIRECTIVES } from '@angular/router';
import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { PAGINATION_DIRECTIVES } from 'ng2-bootstrap/components/pagination';
import { import {
LoaderComponent,
Pagination,
SortField, SortField,
Video, Video,
VideoService VideoService
} from '../shared'; } from '../shared';
import { AuthService, Search, SearchField, User } from '../../shared'; import { AuthService, AuthUser, RestPagination, Search, SearchField } from '../../shared';
import { VideoMiniatureComponent } from './video-miniature.component';
import { VideoSortComponent } from './video-sort.component';
import { SearchService } from '../../shared'; import { SearchService } from '../../shared';
@Component({ @Component({
selector: 'my-videos-list', selector: 'my-videos-list',
styles: [ require('./video-list.component.scss') ], styleUrls: [ './video-list.component.scss' ],
pipes: [ AsyncPipe ], templateUrl: './video-list.component.html'
template: require('./video-list.component.html'),
directives: [ LoaderComponent, PAGINATION_DIRECTIVES, ROUTER_DIRECTIVES, VideoMiniatureComponent, VideoSortComponent ]
}) })
export class VideoListComponent implements OnInit, OnDestroy { export class VideoListComponent implements OnInit, OnDestroy {
loading: BehaviorSubject<boolean> = new BehaviorSubject(false); loading: BehaviorSubject<boolean> = new BehaviorSubject(false);
pagination: Pagination = { pagination: RestPagination = {
currentPage: 1, currentPage: 1,
itemsPerPage: 9, itemsPerPage: 9,
totalItems: null totalItems: null
}; };
sort: SortField; sort: SortField;
user: User = null; user: AuthUser = null;
videos: Video[] = []; videos: Video[] = [];
private search: Search; private search: Search;
@ -51,7 +42,7 @@ export class VideoListComponent implements OnInit, OnDestroy {
ngOnInit() { ngOnInit() {
if (this.authService.isLoggedIn()) { if (this.authService.isLoggedIn()) {
this.user = User.load(); this.user = AuthUser.load();
} }
// Subscribe to route changes // Subscribe to route changes
@ -66,6 +57,8 @@ export class VideoListComponent implements OnInit, OnDestroy {
// Subscribe to search changes // Subscribe to search changes
this.subSearch = this.searchService.searchUpdated.subscribe(search => { this.subSearch = this.searchService.searchUpdated.subscribe(search => {
this.search = search; this.search = search;
// Reset pagination
this.pagination.currentPage = 1;
this.navigateToNewParams(); this.navigateToNewParams();
}); });
@ -76,7 +69,7 @@ export class VideoListComponent implements OnInit, OnDestroy {
this.subSearch.unsubscribe(); this.subSearch.unsubscribe();
} }
getVideos(detectChanges = true) { getVideos() {
this.loading.next(true); this.loading.next(true);
this.videos = []; this.videos = [];
@ -97,7 +90,7 @@ export class VideoListComponent implements OnInit, OnDestroy {
this.loading.next(false); 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.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(); this.changeDetector.detectChanges();
} }

View File

@ -1,16 +1,12 @@
import { DatePipe } from '@angular/common';
import { Component, Input, Output, EventEmitter } from '@angular/core'; import { Component, Input, Output, EventEmitter } from '@angular/core';
import { ROUTER_DIRECTIVES } from '@angular/router';
import { SortField, Video, VideoService } from '../shared'; import { SortField, Video, VideoService } from '../shared';
import { User } from '../../shared'; import { User } from '../../shared';
@Component({ @Component({
selector: 'my-video-miniature', selector: 'my-video-miniature',
styles: [ require('./video-miniature.component.scss') ], styleUrls: [ './video-miniature.component.scss' ],
template: require('./video-miniature.component.html'), templateUrl: './video-miniature.component.html'
directives: [ ROUTER_DIRECTIVES ],
pipes: [ DatePipe ]
}) })
export class VideoMiniatureComponent { export class VideoMiniatureComponent {
@ -40,7 +36,7 @@ export class VideoMiniatureComponent {
if (confirm('Do you really want to remove this video?')) { if (confirm('Do you really want to remove this video?')) {
this.videoService.removeVideo(id).subscribe( this.videoService.removeVideo(id).subscribe(
status => this.removed.emit(true), status => this.removed.emit(true),
error => alert(error) error => alert(error.text)
); );
} }
} }

View File

@ -4,7 +4,7 @@ import { SortField } from '../shared';
@Component({ @Component({
selector: 'my-video-sort', selector: 'my-video-sort',
template: require('./video-sort.component.html') templateUrl: './video-sort.component.html'
}) })
export class VideoSortComponent { export class VideoSortComponent {

View File

@ -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 { ActivatedRoute } from '@angular/router';
import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe'; import { Video, VideoService } from '../shared';
import { LoaderComponent, Video, VideoService } from '../shared';
import { WebTorrentService } from './webtorrent.service'; import { WebTorrentService } from './webtorrent.service';
@Component({ @Component({
selector: 'my-video-watch', selector: 'my-video-watch',
template: require('./video-watch.component.html'), templateUrl: './video-watch.component.html',
styles: [ require('./video-watch.component.scss') ], styleUrls: [ './video-watch.component.scss' ]
providers: [ WebTorrentService ],
directives: [ LoaderComponent ],
pipes: [ BytesPipe ]
}) })
export class VideoWatchComponent implements OnInit, OnDestroy { export class VideoWatchComponent implements OnInit, OnDestroy {
@ -31,6 +26,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
constructor( constructor(
private elementRef: ElementRef, private elementRef: ElementRef,
private ngZone: NgZone,
private route: ActivatedRoute, private route: ActivatedRoute,
private videoService: VideoService, private videoService: VideoService,
private webTorrentService: WebTorrentService private webTorrentService: WebTorrentService
@ -65,12 +61,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
} }
}); });
// Refresh each second this.runInProgress(torrent);
this.torrentInfosInterval = setInterval(() => {
this.downloadSpeed = torrent.downloadSpeed;
this.numPeers = torrent.numPeers;
this.uploadSpeed = torrent.uploadSpeed;
}, 1000);
}); });
} }
@ -91,7 +82,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.video = video; this.video = video;
this.loadVideo(); this.loadVideo();
}, },
error => alert(error) error => alert(error.text)
); );
}); });
} }
@ -100,4 +91,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.error = true; this.error = true;
console.error('The video load seems to be abnormally long.'); 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);
}
} }

View File

@ -1,9 +1,7 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { ROUTER_DIRECTIVES } from '@angular/router';
@Component({ @Component({
template: '<router-outlet></router-outlet>', template: '<router-outlet></router-outlet>'
directives: [ ROUTER_DIRECTIVES ]
}) })
export class VideosComponent { export class VideosComponent {

View File

@ -1,11 +1,11 @@
import { RouterConfig } from '@angular/router'; import { Routes } from '@angular/router';
import { VideoAddComponent } from './video-add'; import { VideoAddComponent } from './video-add';
import { VideoListComponent } from './video-list'; import { VideoListComponent } from './video-list';
import { VideosComponent } from './videos.component'; import { VideosComponent } from './videos.component';
import { VideoWatchComponent } from './video-watch'; import { VideoWatchComponent } from './video-watch';
export const VideosRoutes: RouterConfig = [ export const VideosRoutes: Routes = [
{ {
path: 'videos', path: 'videos',
component: VideosComponent, component: VideosComponent,

View File

@ -1,15 +1,27 @@
/* /*
* Custom Type Definitions * Custom Type Definitions
* When including 3rd party modules you also need to include the type definition for the module * 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 * this file for now. For example
declare module "my-module" { declare module 'my-module' {
export function doesSomething(value: string): string; 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;
} }
* *
@ -17,33 +29,65 @@ declare module "my-module" {
* *
declare var assert: 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 * 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' import * as _ from 'lodash'
* You can include your type definitions in this file until you create one for the typings registry * You can include your type definitions in this file until you create one for the @types
* see https://github.com/typings/registry
* *
*/ */
// support NodeJS modules without type definitions
declare module '*';
// Extra variables that live on Global that will be replaced by webpack DefinePlugin // Extra variables that live on Global that will be replaced by webpack DefinePlugin
declare var ENV: string; declare var ENV: string;
declare var HMR: boolean; declare var HMR: boolean;
declare var System: SystemJS;
interface SystemJS {
import: (path?: string) => Promise<any>;
}
interface GlobalEnvironment { interface GlobalEnvironment {
ENV; ENV;
HMR; 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 { interface WebpackModule {
hot: { hot: {
data?: any, data?: any,
idle: any, idle: any,
accept(dependencies?: string | string[], callback?: (updatedDependencies?: any) => void): void; 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; dispose(callback?: (data?: any) => void): void;
addDisposeHandler(callback?: (data?: any) => void): void; addDisposeHandler(callback?: (data?: any) => void): void;
removeDisposeHandler(callback?: (data?: any) => void): void; removeDisposeHandler(callback?: (data?: any) => void): void;
@ -54,66 +98,26 @@ interface WebpackModule {
}; };
} }
interface WebpackRequire { 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 { interface ErrorStackTraceLimit {
stackTraceLimit: number; stackTraceLimit: number;
} }
// Extend typings // Extend typings
interface NodeRequire extends WebpackRequire {} interface NodeRequire extends WebpackRequire {}
interface ErrorConstructor extends ErrorStackTraceLimit {} interface ErrorConstructor extends ErrorStackTraceLimit {}
interface NodeRequireFunction extends Es6PromiseLoader {}
interface NodeModule extends WebpackModule {} interface NodeModule extends WebpackModule {}
interface Global extends GlobalEnvironment {} 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>;
}

View File

@ -1,3 +1,4 @@
<!DOCTYPE html>
<html> <html>
<head> <head>
<base href="/"> <base href="/">

View File

@ -1,28 +1,20 @@
import { enableProdMode, provide } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { import { decorateModuleRef } from './app/environment';
HTTP_PROVIDERS, import { bootloader } from '@angularclass/hmr';
RequestOptions, /*
XHRBackend * App Module
} from '@angular/http'; * our top level module that holds all of our components
import { bootstrap } from '@angular/platform-browser-dynamic'; */
import { provideRouter } from '@angular/router'; import { AppModule } from './app';
import { AppComponent } from './app/app.component'; /*
import { routes } from './app/app.routes'; * Bootstrap our Angular app with a top level NgModule
import { AuthHttp, AuthService } from './app/shared'; */
export function main(): Promise<any> {
if (process.env.ENV === 'production') { return platformBrowserDynamic()
enableProdMode(); .bootstrapModule(AppModule)
.then(decorateModuleRef)
.catch(err => console.error(err));
} }
bootstrap(AppComponent, [ bootloader(main);
HTTP_PROVIDERS,
provide(AuthHttp, {
useFactory: (backend: XHRBackend, defaultOptions: RequestOptions, authService: AuthService) => {
return new AuthHttp(backend, defaultOptions, authService);
},
deps: [ XHRBackend, RequestOptions, AuthService ]
}),
AuthService,
provideRouter(routes)
]);

Some files were not shown because too many files have changed in this diff Show More