aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src
diff options
context:
space:
mode:
authorChocobozzz <florian.bigard@gmail.com>2016-10-02 15:39:09 +0200
committerChocobozzz <florian.bigard@gmail.com>2016-10-02 15:39:09 +0200
commita6375e69668ea42e19531c6bc68dcd37f3f7cbd7 (patch)
tree03204a408d56311692c3528bedcf95d2455e94f2 /client/src
parent052937db8a8d282eccdbdf38d487ed8d85d3c0a7 (diff)
parentc4403b29ad4db097af528a7f04eea07e0ed320d0 (diff)
downloadPeerTube-a6375e69668ea42e19531c6bc68dcd37f3f7cbd7.tar.gz
PeerTube-a6375e69668ea42e19531c6bc68dcd37f3f7cbd7.tar.zst
PeerTube-a6375e69668ea42e19531c6bc68dcd37f3f7cbd7.zip
Merge branch 'master' into webseed-merged
Diffstat (limited to 'client/src')
-rw-r--r--client/src/app/account/account.component.html27
-rw-r--r--client/src/app/account/account.component.ts67
-rw-r--r--client/src/app/account/account.routes.ts5
-rw-r--r--client/src/app/account/account.service.ts25
-rw-r--r--client/src/app/account/index.ts3
-rw-r--r--client/src/app/admin/admin.component.ts8
-rw-r--r--client/src/app/admin/admin.routes.ts23
-rw-r--r--client/src/app/admin/friends/friend-add/friend-add.component.html26
-rw-r--r--client/src/app/admin/friends/friend-add/friend-add.component.scss7
-rw-r--r--client/src/app/admin/friends/friend-add/friend-add.component.ts108
-rw-r--r--client/src/app/admin/friends/friend-add/index.ts1
-rw-r--r--client/src/app/admin/friends/friend-list/friend-list.component.html29
-rw-r--r--client/src/app/admin/friends/friend-list/friend-list.component.scss3
-rw-r--r--client/src/app/admin/friends/friend-list/friend-list.component.ts38
-rw-r--r--client/src/app/admin/friends/friend-list/index.ts1
-rw-r--r--client/src/app/admin/friends/friends.component.ts8
-rw-r--r--client/src/app/admin/friends/friends.routes.ts27
-rw-r--r--client/src/app/admin/friends/index.ts5
-rw-r--r--client/src/app/admin/friends/shared/friend.model.ts6
-rw-r--r--client/src/app/admin/friends/shared/friend.service.ts39
-rw-r--r--client/src/app/admin/friends/shared/index.ts (renamed from client/src/app/friends/index.ts)1
-rw-r--r--client/src/app/admin/index.ts6
-rw-r--r--client/src/app/admin/menu-admin.component.html26
-rw-r--r--client/src/app/admin/menu-admin.component.ts7
-rw-r--r--client/src/app/admin/requests/index.ts4
-rw-r--r--client/src/app/admin/requests/request-stats/index.ts1
-rw-r--r--client/src/app/admin/requests/request-stats/request-stats.component.html23
-rw-r--r--client/src/app/admin/requests/request-stats/request-stats.component.scss6
-rw-r--r--client/src/app/admin/requests/request-stats/request-stats.component.ts51
-rw-r--r--client/src/app/admin/requests/requests.component.ts8
-rw-r--r--client/src/app/admin/requests/requests.routes.ts22
-rw-r--r--client/src/app/admin/requests/shared/index.ts2
-rw-r--r--client/src/app/admin/requests/shared/request-stats.model.ts32
-rw-r--r--client/src/app/admin/requests/shared/request.service.ts22
-rw-r--r--client/src/app/admin/users/index.ts5
-rw-r--r--client/src/app/admin/users/shared/index.ts1
-rw-r--r--client/src/app/admin/users/shared/user.service.ts47
-rw-r--r--client/src/app/admin/users/user-add/index.ts1
-rw-r--r--client/src/app/admin/users/user-add/user-add.component.html29
-rw-r--r--client/src/app/admin/users/user-add/user-add.component.ts57
-rw-r--r--client/src/app/admin/users/user-list/index.ts1
-rw-r--r--client/src/app/admin/users/user-list/user-list.component.html28
-rw-r--r--client/src/app/admin/users/user-list/user-list.component.scss7
-rw-r--r--client/src/app/admin/users/user-list/user-list.component.ts42
-rw-r--r--client/src/app/admin/users/users.component.ts8
-rw-r--r--client/src/app/admin/users/users.routes.ts27
-rw-r--r--client/src/app/app.component.html38
-rw-r--r--client/src/app/app.component.scss34
-rw-r--r--client/src/app/app.component.ts69
-rw-r--r--client/src/app/app.module.ts146
-rw-r--r--client/src/app/app.routes.ts9
-rw-r--r--client/src/app/app.service.ts36
-rw-r--r--client/src/app/environment.ts50
-rw-r--r--client/src/app/friends/friend.service.ts29
-rw-r--r--client/src/app/index.ts1
-rw-r--r--client/src/app/login/login.component.html19
-rw-r--r--client/src/app/login/login.component.ts58
-rw-r--r--client/src/app/menu.component.html39
-rw-r--r--client/src/app/menu.component.ts45
-rw-r--r--client/src/app/shared/auth/auth-http.service.ts19
-rw-r--r--client/src/app/shared/auth/auth-user.model.ts (renamed from client/src/app/shared/auth/user.model.ts)31
-rw-r--r--client/src/app/shared/auth/auth.service.ts94
-rw-r--r--client/src/app/shared/auth/index.ts2
-rw-r--r--client/src/app/shared/forms/form-reactive.ts24
-rw-r--r--client/src/app/shared/forms/form-validators/index.ts3
-rw-r--r--client/src/app/shared/forms/form-validators/url.validator.ts11
-rw-r--r--client/src/app/shared/forms/form-validators/user.ts17
-rw-r--r--client/src/app/shared/forms/form-validators/video.ts25
-rw-r--r--client/src/app/shared/forms/index.ts2
-rw-r--r--client/src/app/shared/index.ts3
-rw-r--r--client/src/app/shared/rest/index.ts3
-rw-r--r--client/src/app/shared/rest/rest-extractor.service.ts52
-rw-r--r--client/src/app/shared/rest/rest-pagination.ts (renamed from client/src/app/videos/shared/pagination.model.ts)4
-rw-r--r--client/src/app/shared/rest/rest.service.ts27
-rw-r--r--client/src/app/shared/search/search.component.ts16
-rw-r--r--client/src/app/shared/search/search.service.ts3
-rw-r--r--client/src/app/shared/users/index.ts1
-rw-r--r--client/src/app/shared/users/user.model.ts20
-rw-r--r--client/src/app/videos/shared/index.ts1
-rw-r--r--client/src/app/videos/shared/loader/loader.component.ts4
-rw-r--r--client/src/app/videos/shared/video.service.ts62
-rw-r--r--client/src/app/videos/video-add/video-add.component.html30
-rw-r--r--client/src/app/videos/video-add/video-add.component.ts93
-rw-r--r--client/src/app/videos/video-list/video-list.component.ts35
-rw-r--r--client/src/app/videos/video-list/video-miniature.component.ts10
-rw-r--r--client/src/app/videos/video-list/video-sort.component.ts2
-rw-r--r--client/src/app/videos/video-watch/video-watch.component.ts34
-rw-r--r--client/src/app/videos/videos.component.ts4
-rw-r--r--client/src/app/videos/videos.routes.ts4
-rw-r--r--client/src/custom-typings.d.ts118
-rw-r--r--client/src/index.html1
-rw-r--r--client/src/main.ts42
-rw-r--r--client/src/polyfills.ts23
-rw-r--r--client/src/sass/application.scss39
-rw-r--r--client/src/vendor.ts4
95 files changed, 1889 insertions, 467 deletions
diff --git a/client/src/app/account/account.component.html b/client/src/app/account/account.component.html
new file mode 100644
index 000000000..5a8847acd
--- /dev/null
+++ b/client/src/app/account/account.component.html
@@ -0,0 +1,27 @@
1<h3>Account</h3>
2
3<div *ngIf="information" class="alert alert-success">{{ information }}</div>
4<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
5
6<form role="form" (ngSubmit)="changePassword()" [formGroup]="form">
7 <div class="form-group">
8 <label for="new-password">New password</label>
9 <input
10 type="password" class="form-control" id="new-password"
11 formControlName="new-password"
12 >
13 <div *ngIf="formErrors['new-password']" class="alert alert-danger">
14 {{ formErrors['new-password'] }}
15 </div>
16 </div>
17
18 <div class="form-group">
19 <label for="name">Confirm new password</label>
20 <input
21 type="password" class="form-control" id="new-confirmed-password"
22 formControlName="new-confirmed-password"
23 >
24 </div>
25
26 <input type="submit" value="Change password" class="btn btn-default" [disabled]="!form.valid">
27</form>
diff --git a/client/src/app/account/account.component.ts b/client/src/app/account/account.component.ts
new file mode 100644
index 000000000..851eaf198
--- /dev/null
+++ b/client/src/app/account/account.component.ts
@@ -0,0 +1,67 @@
1import { } from '@angular/common';
2import { Component, OnInit } from '@angular/core';
3import { FormBuilder, FormGroup } from '@angular/forms';
4import { Router } from '@angular/router';
5
6import { AccountService } from './account.service';
7import { FormReactive, USER_PASSWORD } from '../shared';
8
9@Component({
10 selector: 'my-account',
11 templateUrl: './account.component.html'
12})
13
14export class AccountComponent extends FormReactive implements OnInit {
15 information: string = null;
16 error: string = null;
17
18 form: FormGroup;
19 formErrors = {
20 'new-password': '',
21 'new-confirmed-password': ''
22 };
23 validationMessages = {
24 'new-password': USER_PASSWORD.MESSAGES,
25 'new-confirmed-password': USER_PASSWORD.MESSAGES
26 };
27
28 constructor(
29 private accountService: AccountService,
30 private formBuilder: FormBuilder,
31 private router: Router
32 ) {
33 super();
34 }
35
36 buildForm() {
37 this.form = this.formBuilder.group({
38 'new-password': [ '', USER_PASSWORD.VALIDATORS ],
39 'new-confirmed-password': [ '', USER_PASSWORD.VALIDATORS ],
40 });
41
42 this.form.valueChanges.subscribe(data => this.onValueChanged(data));
43 }
44
45 ngOnInit() {
46 this.buildForm();
47 }
48
49 changePassword() {
50 const newPassword = this.form.value['new-password'];
51 const newConfirmedPassword = this.form.value['new-confirmed-password'];
52
53 this.information = null;
54 this.error = null;
55
56 if (newPassword !== newConfirmedPassword) {
57 this.error = 'The new password and the confirmed password do not correspond.';
58 return;
59 }
60
61 this.accountService.changePassword(newPassword).subscribe(
62 ok => this.information = 'Password updated.',
63
64 err => this.error = err
65 );
66 }
67}
diff --git a/client/src/app/account/account.routes.ts b/client/src/app/account/account.routes.ts
new file mode 100644
index 000000000..e348c6ebe
--- /dev/null
+++ b/client/src/app/account/account.routes.ts
@@ -0,0 +1,5 @@
1import { AccountComponent } from './account.component';
2
3export const AccountRoutes = [
4 { path: 'account', component: AccountComponent }
5];
diff --git a/client/src/app/account/account.service.ts b/client/src/app/account/account.service.ts
new file mode 100644
index 000000000..355bcef74
--- /dev/null
+++ b/client/src/app/account/account.service.ts
@@ -0,0 +1,25 @@
1import { Injectable } from '@angular/core';
2
3import { AuthHttp, AuthService, RestExtractor } from '../shared';
4
5@Injectable()
6export class AccountService {
7 private static BASE_USERS_URL = '/api/v1/users/';
8
9 constructor(
10 private authHttp: AuthHttp,
11 private authService: AuthService,
12 private restExtractor: RestExtractor
13 ) {}
14
15 changePassword(newPassword: string) {
16 const url = AccountService.BASE_USERS_URL + this.authService.getUser().id;
17 const body = {
18 password: newPassword
19 };
20
21 return this.authHttp.put(url, body)
22 .map(this.restExtractor.extractDataBool)
23 .catch((res) => this.restExtractor.handleError(res));
24 }
25}
diff --git a/client/src/app/account/index.ts b/client/src/app/account/index.ts
new file mode 100644
index 000000000..823d9fe5f
--- /dev/null
+++ b/client/src/app/account/index.ts
@@ -0,0 +1,3 @@
1export * from './account.component';
2export * from './account.routes';
3export * from './account.service';
diff --git a/client/src/app/admin/admin.component.ts b/client/src/app/admin/admin.component.ts
new file mode 100644
index 000000000..64a7400e7
--- /dev/null
+++ b/client/src/app/admin/admin.component.ts
@@ -0,0 +1,8 @@
1import { Component } from '@angular/core';
2
3@Component({
4 template: '<router-outlet></router-outlet>'
5})
6
7export class AdminComponent {
8}
diff --git a/client/src/app/admin/admin.routes.ts b/client/src/app/admin/admin.routes.ts
new file mode 100644
index 000000000..edb8ba49f
--- /dev/null
+++ b/client/src/app/admin/admin.routes.ts
@@ -0,0 +1,23 @@
1import { Routes } from '@angular/router';
2
3import { AdminComponent } from './admin.component';
4import { FriendsRoutes } from './friends';
5import { RequestsRoutes } from './requests';
6import { UsersRoutes } from './users';
7
8export const AdminRoutes: Routes = [
9 {
10 path: 'admin',
11 component: AdminComponent,
12 children: [
13 {
14 path: '',
15 redirectTo: 'users',
16 pathMatch: 'full'
17 },
18 ...FriendsRoutes,
19 ...RequestsRoutes,
20 ...UsersRoutes
21 ]
22 }
23];
diff --git a/client/src/app/admin/friends/friend-add/friend-add.component.html b/client/src/app/admin/friends/friend-add/friend-add.component.html
new file mode 100644
index 000000000..788f3b44d
--- /dev/null
+++ b/client/src/app/admin/friends/friend-add/friend-add.component.html
@@ -0,0 +1,26 @@
1<h3>Make friends</h3>
2
3<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
4
5<form (ngSubmit)="makeFriends()" [formGroup]="form">
6 <div class="form-group" *ngFor="let url of urls; let id = index; trackBy:customTrackBy">
7 <label for="username">Url</label>
8
9 <div class="input-group">
10 <input
11 type="text" class="form-control" placeholder="http://domain.com"
12 [id]="'url-' + id" [formControlName]="'url-' + id"
13 />
14 <span class="input-group-btn">
15 <button *ngIf="displayAddField(id)" (click)="addField()" class="btn btn-default" type="button">+</button>
16 <button *ngIf="displayRemoveField(id)" (click)="removeField(id)" class="btn btn-default" type="button">-</button>
17 </span>
18 </div>
19
20 <div [hidden]="form.controls['url-' + id].valid || form.controls['url-' + id].pristine" class="alert alert-warning">
21 It should be a valid url.
22 </div>
23 </div>
24
25 <input type="submit" value="Make friends" class="btn btn-default" [disabled]="!isFormValid()">
26</form>
diff --git a/client/src/app/admin/friends/friend-add/friend-add.component.scss b/client/src/app/admin/friends/friend-add/friend-add.component.scss
new file mode 100644
index 000000000..5fde51636
--- /dev/null
+++ b/client/src/app/admin/friends/friend-add/friend-add.component.scss
@@ -0,0 +1,7 @@
1table {
2 margin-bottom: 40px;
3}
4
5.input-group-btn button {
6 width: 35px;
7}
diff --git a/client/src/app/admin/friends/friend-add/friend-add.component.ts b/client/src/app/admin/friends/friend-add/friend-add.component.ts
new file mode 100644
index 000000000..64165a9a5
--- /dev/null
+++ b/client/src/app/admin/friends/friend-add/friend-add.component.ts
@@ -0,0 +1,108 @@
1import { Component, OnInit } from '@angular/core';
2import { FormControl, FormGroup } from '@angular/forms';
3import { Router } from '@angular/router';
4
5import { validateUrl } from '../../../shared';
6import { FriendService } from '../shared';
7
8@Component({
9 selector: 'my-friend-add',
10 templateUrl: './friend-add.component.html',
11 styleUrls: [ './friend-add.component.scss' ]
12})
13export class FriendAddComponent implements OnInit {
14 form: FormGroup;
15 urls = [ ];
16 error: string = null;
17
18 constructor(private router: Router, private friendService: FriendService) {}
19
20 ngOnInit() {
21 this.form = new FormGroup({});
22 this.addField();
23 }
24
25 addField() {
26 this.form.addControl(`url-${this.urls.length}`, new FormControl('', [ validateUrl ]));
27 this.urls.push('');
28 }
29
30 customTrackBy(index: number, obj: any): any {
31 return index;
32 }
33
34 displayAddField(index: number) {
35 return index === (this.urls.length - 1);
36 }
37
38 displayRemoveField(index: number) {
39 return (index !== 0 || this.urls.length > 1) && index !== (this.urls.length - 1);
40 }
41
42 isFormValid() {
43 // Do not check the last input
44 for (let i = 0; i < this.urls.length - 1; i++) {
45 if (!this.form.controls[`url-${i}`].valid) return false;
46 }
47
48 const lastIndex = this.urls.length - 1;
49 // If the last input (which is not the first) is empty, it's ok
50 if (this.urls[lastIndex] === '' && lastIndex !== 0) {
51 return true;
52 } else {
53 return this.form.controls[`url-${lastIndex}`].valid;
54 }
55 }
56
57 removeField(index: number) {
58 // Remove the last control
59 this.form.removeControl(`url-${this.urls.length - 1}`);
60 this.urls.splice(index, 1);
61 }
62
63 makeFriends() {
64 this.error = '';
65
66 const notEmptyUrls = this.getNotEmptyUrls();
67 if (notEmptyUrls.length === 0) {
68 this.error = 'You need to specify at less 1 url.';
69 return;
70 }
71
72 if (!this.isUrlsUnique(notEmptyUrls)) {
73 this.error = 'Urls need to be unique.';
74 return;
75 }
76
77 const confirmMessage = 'Are you sure to make friends with:\n - ' + notEmptyUrls.join('\n - ');
78 if (!confirm(confirmMessage)) return;
79
80 this.friendService.makeFriends(notEmptyUrls).subscribe(
81 status => {
82 // TODO: extractdatastatus
83 // if (status === 409) {
84 // alert('Already made friends!');
85 // } else {
86 alert('Make friends request sent!');
87 this.router.navigate([ '/admin/friends/list' ]);
88 // }
89 },
90 error => alert(error.text)
91 );
92 }
93
94 private getNotEmptyUrls() {
95 const notEmptyUrls = [];
96
97 Object.keys(this.form.value).forEach((urlKey) => {
98 const url = this.form.value[urlKey];
99 if (url !== '') notEmptyUrls.push(url);
100 });
101
102 return notEmptyUrls;
103 }
104
105 private isUrlsUnique(urls: string[]) {
106 return urls.every(url => urls.indexOf(url) === urls.lastIndexOf(url));
107 }
108}
diff --git a/client/src/app/admin/friends/friend-add/index.ts b/client/src/app/admin/friends/friend-add/index.ts
new file mode 100644
index 000000000..a101b3be5
--- /dev/null
+++ b/client/src/app/admin/friends/friend-add/index.ts
@@ -0,0 +1 @@
export * from './friend-add.component';
diff --git a/client/src/app/admin/friends/friend-list/friend-list.component.html b/client/src/app/admin/friends/friend-list/friend-list.component.html
new file mode 100644
index 000000000..d786a7846
--- /dev/null
+++ b/client/src/app/admin/friends/friend-list/friend-list.component.html
@@ -0,0 +1,29 @@
1<h3>Friends list</h3>
2
3<table class="table table-hover">
4 <thead>
5 <tr>
6 <th class="table-column-id">ID</th>
7 <th>Url</th>
8 <th>Score</th>
9 <th>Created Date</th>
10 </tr>
11 </thead>
12
13 <tbody>
14 <tr *ngFor="let friend of friends">
15 <td>{{ friend.id }}</td>
16 <td>{{ friend.url }}</td>
17 <td>{{ friend.score }}</td>
18 <td>{{ friend.createdDate | date: 'medium' }}</td>
19 </tr>
20 </tbody>
21</table>
22
23<a *ngIf="friends?.length !== 0" class="add-user btn btn-danger pull-left" (click)="quitFriends()">
24 Quit friends
25</a>
26
27<a *ngIf="friends?.length === 0" class="add-user btn btn-success pull-right" [routerLink]="['/admin/friends/add']">
28 Make friends
29</a>
diff --git a/client/src/app/admin/friends/friend-list/friend-list.component.scss b/client/src/app/admin/friends/friend-list/friend-list.component.scss
new file mode 100644
index 000000000..cb597e12b
--- /dev/null
+++ b/client/src/app/admin/friends/friend-list/friend-list.component.scss
@@ -0,0 +1,3 @@
1table {
2 margin-bottom: 40px;
3}
diff --git a/client/src/app/admin/friends/friend-list/friend-list.component.ts b/client/src/app/admin/friends/friend-list/friend-list.component.ts
new file mode 100644
index 000000000..88c4800ee
--- /dev/null
+++ b/client/src/app/admin/friends/friend-list/friend-list.component.ts
@@ -0,0 +1,38 @@
1import { Component, OnInit } from '@angular/core';
2
3import { Friend, FriendService } from '../shared';
4
5@Component({
6 selector: 'my-friend-list',
7 templateUrl: './friend-list.component.html',
8 styleUrls: [ './friend-list.component.scss' ]
9})
10export class FriendListComponent implements OnInit {
11 friends: Friend[];
12
13 constructor(private friendService: FriendService) { }
14
15 ngOnInit() {
16 this.getFriends();
17 }
18
19 quitFriends() {
20 if (!confirm('Are you sure?')) return;
21
22 this.friendService.quitFriends().subscribe(
23 status => {
24 alert('Quit friends!');
25 this.getFriends();
26 },
27 error => alert(error.text)
28 );
29 }
30
31 private getFriends() {
32 this.friendService.getFriends().subscribe(
33 friends => this.friends = friends,
34
35 err => alert(err.text)
36 );
37 }
38}
diff --git a/client/src/app/admin/friends/friend-list/index.ts b/client/src/app/admin/friends/friend-list/index.ts
new file mode 100644
index 000000000..354c978a4
--- /dev/null
+++ b/client/src/app/admin/friends/friend-list/index.ts
@@ -0,0 +1 @@
export * from './friend-list.component';
diff --git a/client/src/app/admin/friends/friends.component.ts b/client/src/app/admin/friends/friends.component.ts
new file mode 100644
index 000000000..bc3f54158
--- /dev/null
+++ b/client/src/app/admin/friends/friends.component.ts
@@ -0,0 +1,8 @@
1import { Component } from '@angular/core';
2
3@Component({
4 template: '<router-outlet></router-outlet>'
5})
6
7export class FriendsComponent {
8}
diff --git a/client/src/app/admin/friends/friends.routes.ts b/client/src/app/admin/friends/friends.routes.ts
new file mode 100644
index 000000000..7fdef68f9
--- /dev/null
+++ b/client/src/app/admin/friends/friends.routes.ts
@@ -0,0 +1,27 @@
1import { Routes } from '@angular/router';
2
3import { FriendsComponent } from './friends.component';
4import { FriendAddComponent } from './friend-add';
5import { FriendListComponent } from './friend-list';
6
7export const FriendsRoutes: Routes = [
8 {
9 path: 'friends',
10 component: FriendsComponent,
11 children: [
12 {
13 path: '',
14 redirectTo: 'list',
15 pathMatch: 'full'
16 },
17 {
18 path: 'list',
19 component: FriendListComponent
20 },
21 {
22 path: 'add',
23 component: FriendAddComponent
24 }
25 ]
26 }
27];
diff --git a/client/src/app/admin/friends/index.ts b/client/src/app/admin/friends/index.ts
new file mode 100644
index 000000000..dd4df2538
--- /dev/null
+++ b/client/src/app/admin/friends/index.ts
@@ -0,0 +1,5 @@
1export * from './friend-add';
2export * from './friend-list';
3export * from './shared';
4export * from './friends.component';
5export * from './friends.routes';
diff --git a/client/src/app/admin/friends/shared/friend.model.ts b/client/src/app/admin/friends/shared/friend.model.ts
new file mode 100644
index 000000000..7cb28f440
--- /dev/null
+++ b/client/src/app/admin/friends/shared/friend.model.ts
@@ -0,0 +1,6 @@
1export interface Friend {
2 id: string;
3 url: string;
4 score: number;
5 createdDate: Date;
6}
diff --git a/client/src/app/admin/friends/shared/friend.service.ts b/client/src/app/admin/friends/shared/friend.service.ts
new file mode 100644
index 000000000..75826fc17
--- /dev/null
+++ b/client/src/app/admin/friends/shared/friend.service.ts
@@ -0,0 +1,39 @@
1import { Injectable } from '@angular/core';
2import { Observable } from 'rxjs/Observable';
3
4import { Friend } from './friend.model';
5import { AuthHttp, RestExtractor } from '../../../shared';
6
7@Injectable()
8export class FriendService {
9 private static BASE_FRIEND_URL: string = '/api/v1/pods/';
10
11 constructor (
12 private authHttp: AuthHttp,
13 private restExtractor: RestExtractor
14 ) {}
15
16 getFriends(): Observable<Friend[]> {
17 return this.authHttp.get(FriendService.BASE_FRIEND_URL)
18 // Not implemented as a data list by the server yet
19 // .map(this.restExtractor.extractDataList)
20 .map((res) => res.json())
21 .catch((res) => this.restExtractor.handleError(res));
22 }
23
24 makeFriends(notEmptyUrls) {
25 const body = {
26 urls: notEmptyUrls
27 };
28
29 return this.authHttp.post(FriendService.BASE_FRIEND_URL + 'makefriends', body)
30 .map(this.restExtractor.extractDataBool)
31 .catch((res) => this.restExtractor.handleError(res));
32 }
33
34 quitFriends() {
35 return this.authHttp.get(FriendService.BASE_FRIEND_URL + 'quitfriends')
36 .map(res => res.status)
37 .catch((res) => this.restExtractor.handleError(res));
38 }
39}
diff --git a/client/src/app/friends/index.ts b/client/src/app/admin/friends/shared/index.ts
index 0adc256c4..0d671637d 100644
--- a/client/src/app/friends/index.ts
+++ b/client/src/app/admin/friends/shared/index.ts
@@ -1 +1,2 @@
1export * from './friend.model';
1export * from './friend.service'; 2export * from './friend.service';
diff --git a/client/src/app/admin/index.ts b/client/src/app/admin/index.ts
new file mode 100644
index 000000000..493caed15
--- /dev/null
+++ b/client/src/app/admin/index.ts
@@ -0,0 +1,6 @@
1export * from './friends';
2export * from './requests';
3export * from './users';
4export * from './admin.component';
5export * from './admin.routes';
6export * from './menu-admin.component';
diff --git a/client/src/app/admin/menu-admin.component.html b/client/src/app/admin/menu-admin.component.html
new file mode 100644
index 000000000..e250615aa
--- /dev/null
+++ b/client/src/app/admin/menu-admin.component.html
@@ -0,0 +1,26 @@
1<menu class="col-md-2 col-sm-3 col-xs-3">
2
3 <div class="panel-block">
4 <div id="panel-users" class="panel-button">
5 <span class="hidden-xs glyphicon glyphicon-user"></span>
6 <a [routerLink]="['/admin/users/list']">List users</a>
7 </div>
8
9 <div id="panel-friends" class="panel-button">
10 <span class="hidden-xs glyphicon glyphicon-cloud"></span>
11 <a [routerLink]="['/admin/friends/list']">List friends</a>
12 </div>
13
14 <div id="panel-request-stats" class="panel-button">
15 <span class="hidden-xs glyphicon glyphicon-stats"></span>
16 <a [routerLink]="['/admin/requests/stats']">Request stats</a>
17 </div>
18 </div>
19
20 <div class="panel-block">
21 <div id="panel-quit-administration" class="panel-button">
22 <span class="hidden-xs glyphicon glyphicon-cog"></span>
23 <a [routerLink]="['/videos/list']">Quit admin.</a>
24 </div>
25 </div>
26</menu>
diff --git a/client/src/app/admin/menu-admin.component.ts b/client/src/app/admin/menu-admin.component.ts
new file mode 100644
index 000000000..59ffccf9f
--- /dev/null
+++ b/client/src/app/admin/menu-admin.component.ts
@@ -0,0 +1,7 @@
1import { Component } from '@angular/core';
2
3@Component({
4 selector: 'my-menu-admin',
5 templateUrl: './menu-admin.component.html'
6})
7export class MenuAdminComponent { }
diff --git a/client/src/app/admin/requests/index.ts b/client/src/app/admin/requests/index.ts
new file mode 100644
index 000000000..236a9ee8f
--- /dev/null
+++ b/client/src/app/admin/requests/index.ts
@@ -0,0 +1,4 @@
1export * from './request-stats';
2export * from './shared';
3export * from './requests.component';
4export * from './requests.routes';
diff --git a/client/src/app/admin/requests/request-stats/index.ts b/client/src/app/admin/requests/request-stats/index.ts
new file mode 100644
index 000000000..be3a66f77
--- /dev/null
+++ b/client/src/app/admin/requests/request-stats/index.ts
@@ -0,0 +1 @@
export * from './request-stats.component';
diff --git a/client/src/app/admin/requests/request-stats/request-stats.component.html b/client/src/app/admin/requests/request-stats/request-stats.component.html
new file mode 100644
index 000000000..b5ac59a9a
--- /dev/null
+++ b/client/src/app/admin/requests/request-stats/request-stats.component.html
@@ -0,0 +1,23 @@
1<h3>Requests stats</h3>
2
3<div *ngIf="stats !== null">
4 <div>
5 <span class="label-description">Interval seconds between requests:</span>
6 {{ stats.secondsInterval }}
7 </div>
8
9 <div>
10 <span class="label-description">Remaining time before the scheduled request:</span>
11 {{ stats.remainingSeconds }}
12 </div>
13
14 <div>
15 <span class="label-description">Maximum number of requests per interval:</span>
16 {{ stats.maxRequestsInParallel }}
17 </div>
18
19 <div>
20 <span class="label-description">Remaining requests:</span>
21 {{ stats.requests.length }}
22 </div>
23</div>
diff --git a/client/src/app/admin/requests/request-stats/request-stats.component.scss b/client/src/app/admin/requests/request-stats/request-stats.component.scss
new file mode 100644
index 000000000..92c28dc99
--- /dev/null
+++ b/client/src/app/admin/requests/request-stats/request-stats.component.scss
@@ -0,0 +1,6 @@
1.label-description {
2 display: inline-block;
3 width: 350px;
4 font-weight: bold;
5 color: black;
6}
diff --git a/client/src/app/admin/requests/request-stats/request-stats.component.ts b/client/src/app/admin/requests/request-stats/request-stats.component.ts
new file mode 100644
index 000000000..4b0844574
--- /dev/null
+++ b/client/src/app/admin/requests/request-stats/request-stats.component.ts
@@ -0,0 +1,51 @@
1import { Component, OnInit, OnDestroy } from '@angular/core';
2
3import { RequestService, RequestStats } from '../shared';
4
5@Component({
6 selector: 'my-request-stats',
7 templateUrl: './request-stats.component.html',
8 styleUrls: [ './request-stats.component.scss' ]
9})
10export class RequestStatsComponent implements OnInit, OnDestroy {
11 stats: RequestStats = null;
12
13 private interval: NodeJS.Timer = null;
14
15 constructor(private requestService: RequestService) { }
16
17 ngOnInit() {
18 this.getStats();
19 }
20
21 ngOnDestroy() {
22 if (this.stats.secondsInterval !== null) {
23 clearInterval(this.interval);
24 }
25 }
26
27 getStats() {
28 this.requestService.getStats().subscribe(
29 stats => {
30 console.log(stats);
31 this.stats = stats;
32 this.runInterval();
33 },
34
35 err => alert(err.text)
36 );
37 }
38
39 private runInterval() {
40 this.interval = setInterval(() => {
41 this.stats.remainingMilliSeconds -= 1000;
42
43 if (this.stats.remainingMilliSeconds <= 0) {
44 setTimeout(() => this.getStats(), this.stats.remainingMilliSeconds + 100);
45 clearInterval(this.interval);
46 }
47 }, 1000);
48 }
49
50
51}
diff --git a/client/src/app/admin/requests/requests.component.ts b/client/src/app/admin/requests/requests.component.ts
new file mode 100644
index 000000000..471112b45
--- /dev/null
+++ b/client/src/app/admin/requests/requests.component.ts
@@ -0,0 +1,8 @@
1import { Component } from '@angular/core';
2
3@Component({
4 template: '<router-outlet></router-outlet>'
5})
6
7export class RequestsComponent {
8}
diff --git a/client/src/app/admin/requests/requests.routes.ts b/client/src/app/admin/requests/requests.routes.ts
new file mode 100644
index 000000000..78221a9ff
--- /dev/null
+++ b/client/src/app/admin/requests/requests.routes.ts
@@ -0,0 +1,22 @@
1import { Routes } from '@angular/router';
2
3import { RequestsComponent } from './requests.component';
4import { RequestStatsComponent } from './request-stats';
5
6export const RequestsRoutes: Routes = [
7 {
8 path: 'requests',
9 component: RequestsComponent,
10 children: [
11 {
12 path: '',
13 redirectTo: 'stats',
14 pathMatch: 'full'
15 },
16 {
17 path: 'stats',
18 component: RequestStatsComponent
19 }
20 ]
21 }
22];
diff --git a/client/src/app/admin/requests/shared/index.ts b/client/src/app/admin/requests/shared/index.ts
new file mode 100644
index 000000000..32ab5767b
--- /dev/null
+++ b/client/src/app/admin/requests/shared/index.ts
@@ -0,0 +1,2 @@
1export * from './request-stats.model';
2export * from './request.service';
diff --git a/client/src/app/admin/requests/shared/request-stats.model.ts b/client/src/app/admin/requests/shared/request-stats.model.ts
new file mode 100644
index 000000000..766e80836
--- /dev/null
+++ b/client/src/app/admin/requests/shared/request-stats.model.ts
@@ -0,0 +1,32 @@
1export interface Request {
2 request: any;
3 to: any;
4}
5
6export class RequestStats {
7 maxRequestsInParallel: number;
8 milliSecondsInterval: number;
9 remainingMilliSeconds: number;
10 requests: Request[];
11
12 constructor(hash: {
13 maxRequestsInParallel: number,
14 milliSecondsInterval: number,
15 remainingMilliSeconds: number,
16 requests: Request[];
17 }) {
18 this.maxRequestsInParallel = hash.maxRequestsInParallel;
19 this.milliSecondsInterval = hash.milliSecondsInterval;
20 this.remainingMilliSeconds = hash.remainingMilliSeconds;
21 this.requests = hash.requests;
22 }
23
24 get remainingSeconds() {
25 return Math.floor(this.remainingMilliSeconds / 1000);
26 }
27
28 get secondsInterval() {
29 return Math.floor(this.milliSecondsInterval / 1000);
30 }
31
32}
diff --git a/client/src/app/admin/requests/shared/request.service.ts b/client/src/app/admin/requests/shared/request.service.ts
new file mode 100644
index 000000000..aeec37448
--- /dev/null
+++ b/client/src/app/admin/requests/shared/request.service.ts
@@ -0,0 +1,22 @@
1import { Injectable } from '@angular/core';
2import { Observable } from 'rxjs/Observable';
3
4import { RequestStats } from './request-stats.model';
5import { AuthHttp, RestExtractor } from '../../../shared';
6
7@Injectable()
8export class RequestService {
9 private static BASE_REQUEST_URL: string = '/api/v1/requests/';
10
11 constructor (
12 private authHttp: AuthHttp,
13 private restExtractor: RestExtractor
14 ) {}
15
16 getStats(): Observable<RequestStats> {
17 return this.authHttp.get(RequestService.BASE_REQUEST_URL + 'stats')
18 .map(this.restExtractor.extractDataGet)
19 .map((data) => new RequestStats(data))
20 .catch((res) => this.restExtractor.handleError(res));
21 }
22}
diff --git a/client/src/app/admin/users/index.ts b/client/src/app/admin/users/index.ts
new file mode 100644
index 000000000..e98a81f62
--- /dev/null
+++ b/client/src/app/admin/users/index.ts
@@ -0,0 +1,5 @@
1export * from './shared';
2export * from './user-add';
3export * from './user-list';
4export * from './users.component';
5export * from './users.routes';
diff --git a/client/src/app/admin/users/shared/index.ts b/client/src/app/admin/users/shared/index.ts
new file mode 100644
index 000000000..e17ee5c7a
--- /dev/null
+++ b/client/src/app/admin/users/shared/index.ts
@@ -0,0 +1 @@
export * from './user.service';
diff --git a/client/src/app/admin/users/shared/user.service.ts b/client/src/app/admin/users/shared/user.service.ts
new file mode 100644
index 000000000..13be553c0
--- /dev/null
+++ b/client/src/app/admin/users/shared/user.service.ts
@@ -0,0 +1,47 @@
1import { Injectable } from '@angular/core';
2
3import { AuthHttp, RestExtractor, ResultList, User } from '../../../shared';
4
5@Injectable()
6export class UserService {
7 // TODO: merge this constant with account
8 private static BASE_USERS_URL = '/api/v1/users/';
9
10 constructor(
11 private authHttp: AuthHttp,
12 private restExtractor: RestExtractor
13 ) {}
14
15 addUser(username: string, password: string) {
16 const body = {
17 username,
18 password
19 };
20
21 return this.authHttp.post(UserService.BASE_USERS_URL, body)
22 .map(this.restExtractor.extractDataBool)
23 .catch(this.restExtractor.handleError);
24 }
25
26 getUsers() {
27 return this.authHttp.get(UserService.BASE_USERS_URL)
28 .map(this.restExtractor.extractDataList)
29 .map(this.extractUsers)
30 .catch((res) => this.restExtractor.handleError(res));
31 }
32
33 removeUser(user: User) {
34 return this.authHttp.delete(UserService.BASE_USERS_URL + user.id);
35 }
36
37 private extractUsers(result: ResultList) {
38 const usersJson = result.data;
39 const totalUsers = result.total;
40 const users = [];
41 for (const userJson of usersJson) {
42 users.push(new User(userJson));
43 }
44
45 return { users, totalUsers };
46 }
47}
diff --git a/client/src/app/admin/users/user-add/index.ts b/client/src/app/admin/users/user-add/index.ts
new file mode 100644
index 000000000..66d5ca04f
--- /dev/null
+++ b/client/src/app/admin/users/user-add/index.ts
@@ -0,0 +1 @@
export * from './user-add.component';
diff --git a/client/src/app/admin/users/user-add/user-add.component.html b/client/src/app/admin/users/user-add/user-add.component.html
new file mode 100644
index 000000000..9b76c7c1b
--- /dev/null
+++ b/client/src/app/admin/users/user-add/user-add.component.html
@@ -0,0 +1,29 @@
1<h3>Add user</h3>
2
3<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
4
5<form role="form" (ngSubmit)="addUser()" [formGroup]="form">
6 <div class="form-group">
7 <label for="username">Username</label>
8 <input
9 type="text" class="form-control" id="username" placeholder="Username"
10 formControlName="username"
11 >
12 <div *ngIf="formErrors.username" class="alert alert-danger">
13 {{ formErrors.username }}
14 </div>
15 </div>
16
17 <div class="form-group">
18 <label for="password">Password</label>
19 <input
20 type="password" class="form-control" id="password" placeholder="Password"
21 formControlName="password"
22 >
23 <div *ngIf="formErrors.password" class="alert alert-danger">
24 {{ formErrors.password }}
25 </div>
26 </div>
27
28 <input type="submit" value="Add user" class="btn btn-default" [disabled]="!form.valid">
29</form>
diff --git a/client/src/app/admin/users/user-add/user-add.component.ts b/client/src/app/admin/users/user-add/user-add.component.ts
new file mode 100644
index 000000000..ab96fb01d
--- /dev/null
+++ b/client/src/app/admin/users/user-add/user-add.component.ts
@@ -0,0 +1,57 @@
1import { Component, OnInit } from '@angular/core';
2import { FormBuilder, FormGroup } from '@angular/forms';
3import { Router } from '@angular/router';
4
5import { UserService } from '../shared';
6import { FormReactive, USER_USERNAME, USER_PASSWORD } from '../../../shared';
7
8@Component({
9 selector: 'my-user-add',
10 templateUrl: './user-add.component.html'
11})
12export class UserAddComponent extends FormReactive implements OnInit {
13 error: string = null;
14
15 form: FormGroup;
16 formErrors = {
17 'username': '',
18 'password': ''
19 };
20 validationMessages = {
21 'username': USER_USERNAME.MESSAGES,
22 'password': USER_PASSWORD.MESSAGES,
23 };
24
25 constructor(
26 private formBuilder: FormBuilder,
27 private router: Router,
28 private userService: UserService
29 ) {
30 super();
31 }
32
33 buildForm() {
34 this.form = this.formBuilder.group({
35 username: [ '', USER_USERNAME.VALIDATORS ],
36 password: [ '', USER_PASSWORD.VALIDATORS ],
37 });
38
39 this.form.valueChanges.subscribe(data => this.onValueChanged(data));
40 }
41
42 ngOnInit() {
43 this.buildForm();
44 }
45
46 addUser() {
47 this.error = null;
48
49 const { username, password } = this.form.value;
50
51 this.userService.addUser(username, password).subscribe(
52 ok => this.router.navigate([ '/admin/users/list' ]),
53
54 err => this.error = err.text
55 );
56 }
57}
diff --git a/client/src/app/admin/users/user-list/index.ts b/client/src/app/admin/users/user-list/index.ts
new file mode 100644
index 000000000..51fbefa80
--- /dev/null
+++ b/client/src/app/admin/users/user-list/index.ts
@@ -0,0 +1 @@
export * from './user-list.component';
diff --git a/client/src/app/admin/users/user-list/user-list.component.html b/client/src/app/admin/users/user-list/user-list.component.html
new file mode 100644
index 000000000..328b1be77
--- /dev/null
+++ b/client/src/app/admin/users/user-list/user-list.component.html
@@ -0,0 +1,28 @@
1<h3>Users list</h3>
2
3<table class="table table-hover">
4 <thead>
5 <tr>
6 <th class="table-column-id">ID</th>
7 <th>Username</th>
8 <th>Created Date</th>
9 <th class="text-right">Remove</th>
10 </tr>
11 </thead>
12
13 <tbody>
14 <tr *ngFor="let user of users">
15 <td>{{ user.id }}</td>
16 <td>{{ user.username }}</td>
17 <td>{{ user.createdDate | date: 'medium' }}</td>
18 <td class="text-right">
19 <span class="glyphicon glyphicon-remove" *ngIf="!user.isAdmin()" (click)="removeUser(user)"></span>
20 </td>
21 </tr>
22 </tbody>
23</table>
24
25<a class="add-user btn btn-success pull-right" [routerLink]="['/admin/users/add']">
26 <span class="glyphicon glyphicon-plus"></span>
27 Add user
28</a>
diff --git a/client/src/app/admin/users/user-list/user-list.component.scss b/client/src/app/admin/users/user-list/user-list.component.scss
new file mode 100644
index 000000000..e9f61e900
--- /dev/null
+++ b/client/src/app/admin/users/user-list/user-list.component.scss
@@ -0,0 +1,7 @@
1.glyphicon-remove {
2 cursor: pointer;
3}
4
5.add-user {
6 margin-top: 10px;
7}
diff --git a/client/src/app/admin/users/user-list/user-list.component.ts b/client/src/app/admin/users/user-list/user-list.component.ts
new file mode 100644
index 000000000..03f4e5c0a
--- /dev/null
+++ b/client/src/app/admin/users/user-list/user-list.component.ts
@@ -0,0 +1,42 @@
1import { Component, OnInit } from '@angular/core';
2
3import { User } from '../../../shared';
4import { UserService } from '../shared';
5
6@Component({
7 selector: 'my-user-list',
8 templateUrl: './user-list.component.html',
9 styleUrls: [ './user-list.component.scss' ]
10})
11export class UserListComponent implements OnInit {
12 totalUsers: number;
13 users: User[];
14
15 constructor(private userService: UserService) {}
16
17 ngOnInit() {
18 this.getUsers();
19 }
20
21 getUsers() {
22 this.userService.getUsers().subscribe(
23 ({ users, totalUsers }) => {
24 this.users = users;
25 this.totalUsers = totalUsers;
26 },
27
28 err => alert(err.text)
29 );
30 }
31
32
33 removeUser(user: User) {
34 if (confirm('Are you sure?')) {
35 this.userService.removeUser(user).subscribe(
36 () => this.getUsers(),
37
38 err => alert(err.text)
39 );
40 }
41 }
42}
diff --git a/client/src/app/admin/users/users.component.ts b/client/src/app/admin/users/users.component.ts
new file mode 100644
index 000000000..37e3b158d
--- /dev/null
+++ b/client/src/app/admin/users/users.component.ts
@@ -0,0 +1,8 @@
1import { Component } from '@angular/core';
2
3@Component({
4 template: '<router-outlet></router-outlet>'
5})
6
7export class UsersComponent {
8}
diff --git a/client/src/app/admin/users/users.routes.ts b/client/src/app/admin/users/users.routes.ts
new file mode 100644
index 000000000..eb71bd0ae
--- /dev/null
+++ b/client/src/app/admin/users/users.routes.ts
@@ -0,0 +1,27 @@
1import { Routes } from '@angular/router';
2
3import { UsersComponent } from './users.component';
4import { UserAddComponent } from './user-add';
5import { UserListComponent } from './user-list';
6
7export const UsersRoutes: Routes = [
8 {
9 path: 'users',
10 component: UsersComponent,
11 children: [
12 {
13 path: '',
14 redirectTo: 'list',
15 pathMatch: 'full'
16 },
17 {
18 path: 'list',
19 component: UserListComponent
20 },
21 {
22 path: 'add',
23 component: UserAddComponent
24 }
25 ]
26 }
27];
diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html
index f2acffea4..04c32f596 100644
--- a/client/src/app/app.component.html
+++ b/client/src/app/app.component.html
@@ -14,48 +14,14 @@
14 14
15 15
16 <div class="row"> 16 <div class="row">
17 17 <my-menu *ngIf="isInAdmin() === false"></my-menu>
18 <menu class="col-md-2 col-sm-3 col-xs-3"> 18 <my-menu-admin *ngIf="isInAdmin() === true"></my-menu-admin>
19 <div class="panel-block">
20 <div id="panel-user-login" class="panel-button">
21 <span class="hidden-xs glyphicon glyphicon-user"></span>
22 <a *ngIf="!isLoggedIn" [routerLink]="['/login']">Login</a>
23 <a *ngIf="isLoggedIn" (click)="logout()">Logout</a>
24 </div>
25 </div>
26
27 <div class="panel-block">
28 <div id="panel-get-videos" class="panel-button">
29 <span class="hidden-xs glyphicon glyphicon-list"></span>
30 <a [routerLink]="['/videos/list']">Get videos</a>
31 </div>
32
33 <div id="panel-upload-video" class="panel-button" *ngIf="isLoggedIn">
34 <span class="hidden-xs glyphicon glyphicon-cloud-upload"></span>
35 <a [routerLink]="['/videos/add']">Upload a video</a>
36 </div>
37 </div>
38
39 <div class="panel-block" *ngIf="isLoggedIn">
40 <div id="panel-make-friends" class="panel-button">
41 <span class="hidden-xs glyphicon glyphicon-cloud"></span>
42 <a (click)='makeFriends()'>Make friends</a>
43 </div>
44
45 <div id="panel-quit-friends" class="panel-button">
46 <span class="hidden-xs glyphicon glyphicon-plane"></span>
47 <a (click)='quitFriends()'>Quit friends</a>
48 </div>
49 </div>
50 </menu>
51 19
52 <div class="col-md-9 col-sm-8 col-xs-8 router-outlet-container"> 20 <div class="col-md-9 col-sm-8 col-xs-8 router-outlet-container">
53 <router-outlet></router-outlet> 21 <router-outlet></router-outlet>
54 </div> 22 </div>
55
56 </div> 23 </div>
57 24
58
59 <footer> 25 <footer>
60 PeerTube, CopyLeft 2015-2016 26 PeerTube, CopyLeft 2015-2016
61 </footer> 27 </footer>
diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss
index 1b02b2f57..95f306d75 100644
--- a/client/src/app/app.component.scss
+++ b/client/src/app/app.component.scss
@@ -12,40 +12,6 @@ header div {
12 margin-bottom: 30px; 12 margin-bottom: 30px;
13} 13}
14 14
15menu {
16 @media screen and (max-width: 600px) {
17 margin-right: 3px !important;
18 padding: 3px !important;
19 min-height: 400px !important;
20 }
21
22 min-height: 600px;
23 margin-right: 20px;
24 border-right: 1px solid rgba(0, 0, 0, 0.2);
25
26 .panel-button {
27 margin: 8px;
28 cursor: pointer;
29 transition: margin 0.2s;
30
31 &:hover {
32 margin-left: 15px;
33 }
34
35 a {
36 color: #333333;
37 }
38 }
39
40 .glyphicon {
41 margin: 5px;
42 }
43}
44
45.panel-block:not(:last-child) {
46 border-bottom: 1px solid rgba(0, 0, 0, 0.1);
47}
48
49.router-outlet-container { 15.router-outlet-container {
50 @media screen and (max-width: 400px) { 16 @media screen and (max-width: 400px) {
51 padding: 0 3px 0 3px; 17 padding: 0 3px 0 3px;
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts
index b7a3d7c58..d6b83c684 100644
--- a/client/src/app/app.component.ts
+++ b/client/src/app/app.component.ts
@@ -1,73 +1,16 @@
1import { Component } from '@angular/core'; 1import { Component } from '@angular/core';
2import { ActivatedRoute, Router, ROUTER_DIRECTIVES } from '@angular/router'; 2import { Router } from '@angular/router';
3
4import { FriendService } from './friends';
5import {
6 AuthService,
7 AuthStatus,
8 SearchComponent,
9 SearchService
10} from './shared';
11import { VideoService } from './videos';
12 3
13@Component({ 4@Component({
14 selector: 'my-app', 5 selector: 'my-app',
15 template: require('./app.component.html'), 6 templateUrl: './app.component.html',
16 styles: [ require('./app.component.scss') ], 7 styleUrls: [ './app.component.scss' ]
17 directives: [ ROUTER_DIRECTIVES, SearchComponent ],
18 providers: [ FriendService, VideoService, SearchService ]
19}) 8})
20 9
21export class AppComponent { 10export class AppComponent {
22 choices = []; 11 constructor(private router: Router) {}
23 isLoggedIn: boolean;
24
25 constructor(
26 private authService: AuthService,
27 private friendService: FriendService,
28 private route: ActivatedRoute,
29 private router: Router
30 ) {
31 this.isLoggedIn = this.authService.isLoggedIn();
32
33 this.authService.loginChangedSource.subscribe(
34 status => {
35 if (status === AuthStatus.LoggedIn) {
36 this.isLoggedIn = true;
37 console.log('Logged in.');
38 } else if (status === AuthStatus.LoggedOut) {
39 this.isLoggedIn = false;
40 console.log('Logged out.');
41 } else {
42 console.error('Unknown auth status: ' + status);
43 }
44 }
45 );
46 }
47
48 logout() {
49 this.authService.logout();
50 }
51
52 makeFriends() {
53 this.friendService.makeFriends().subscribe(
54 status => {
55 if (status === 409) {
56 alert('Already made friends!');
57 } else {
58 alert('Made friends!');
59 }
60 },
61 error => alert(error)
62 );
63 }
64 12
65 quitFriends() { 13 isInAdmin() {
66 this.friendService.quitFriends().subscribe( 14 return this.router.url.indexOf('/admin/') !== -1;
67 status => {
68 alert('Quit friends!');
69 },
70 error => alert(error)
71 );
72 } 15 }
73} 16}
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts
new file mode 100644
index 000000000..980625f13
--- /dev/null
+++ b/client/src/app/app.module.ts
@@ -0,0 +1,146 @@
1import { ApplicationRef, NgModule } from '@angular/core';
2import { BrowserModule } from '@angular/platform-browser';
3import { FormsModule, ReactiveFormsModule } from '@angular/forms';
4import { HttpModule, RequestOptions, XHRBackend } from '@angular/http';
5import { RouterModule } from '@angular/router';
6import { removeNgStyles, createNewHosts } from '@angularclass/hmr';
7
8import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe';
9import { ProgressbarModule } from 'ng2-bootstrap/components/progressbar';
10import { PaginationModule } from 'ng2-bootstrap/components/pagination';
11import { FileUploadModule } from 'ng2-file-upload/ng2-file-upload';
12
13/*
14 * Platform and Environment providers/directives/pipes
15 */
16import { ENV_PROVIDERS } from './environment';
17import { routes } from './app.routes';
18// App is our top level component
19import { AppComponent } from './app.component';
20import { AppState } from './app.service';
21
22import {
23 AdminComponent,
24 FriendsComponent,
25 FriendAddComponent,
26 FriendListComponent,
27 FriendService,
28 MenuAdminComponent,
29 RequestsComponent,
30 RequestStatsComponent,
31 RequestService,
32 UsersComponent,
33 UserAddComponent,
34 UserListComponent,
35 UserService
36} from './admin';
37import { AccountComponent, AccountService } from './account';
38import { LoginComponent } from './login';
39import { MenuComponent } from './menu.component';
40import { AuthService, AuthHttp, RestExtractor, RestService, SearchComponent, SearchService } from './shared';
41import {
42 LoaderComponent,
43 VideosComponent,
44 VideoAddComponent,
45 VideoListComponent,
46 VideoMiniatureComponent,
47 VideoSortComponent,
48 VideoWatchComponent,
49 VideoService,
50 WebTorrentService
51} from './videos';
52
53// Application wide providers
54const APP_PROVIDERS = [
55 AppState,
56
57 {
58 provide: AuthHttp,
59 useFactory: (backend: XHRBackend, defaultOptions: RequestOptions, authService: AuthService) => {
60 return new AuthHttp(backend, defaultOptions, authService);
61 },
62 deps: [ XHRBackend, RequestOptions, AuthService ]
63 },
64
65 AuthService,
66 RestExtractor,
67 RestService,
68
69 VideoService,
70 SearchService,
71 FriendService,
72 RequestService,
73 UserService,
74 AccountService,
75 WebTorrentService
76];
77/**
78 * `AppModule` is the main entry point into Angular2's bootstraping process
79 */
80@NgModule({
81 bootstrap: [ AppComponent ],
82 declarations: [
83 AccountComponent,
84 AdminComponent,
85 AppComponent,
86 BytesPipe,
87 FriendAddComponent,
88 FriendListComponent,
89 FriendsComponent,
90 LoaderComponent,
91 LoginComponent,
92 MenuAdminComponent,
93 MenuComponent,
94 RequestsComponent,
95 RequestStatsComponent,
96 SearchComponent,
97 UserAddComponent,
98 UserListComponent,
99 UsersComponent,
100 VideoAddComponent,
101 VideoListComponent,
102 VideoMiniatureComponent,
103 VideosComponent,
104 VideoSortComponent,
105 VideoWatchComponent,
106 ],
107 imports: [ // import Angular's modules
108 BrowserModule,
109 FormsModule,
110 ReactiveFormsModule,
111 HttpModule,
112 RouterModule.forRoot(routes),
113
114 ProgressbarModule,
115 PaginationModule,
116 FileUploadModule
117 ],
118 providers: [ // expose our Services and Providers into Angular's dependency injection
119 ENV_PROVIDERS,
120 APP_PROVIDERS
121 ]
122})
123export class AppModule {
124 constructor(public appRef: ApplicationRef, public appState: AppState) {}
125 hmrOnInit(store) {
126 if (!store || !store.state) return;
127 console.log('HMR store', store);
128 this.appState._state = store.state;
129 this.appRef.tick();
130 delete store.state;
131 }
132 hmrOnDestroy(store) {
133 const cmpLocation = this.appRef.components.map(cmp => cmp.location.nativeElement);
134 // recreate elements
135 const state = this.appState._state;
136 store.state = state;
137 store.disposeOldHosts = createNewHosts(cmpLocation);
138 // remove styles
139 removeNgStyles();
140 }
141 hmrAfterDestroy(store) {
142 // display new elements
143 store.disposeOldHosts();
144 delete store.disposeOldHosts;
145 }
146}
diff --git a/client/src/app/app.routes.ts b/client/src/app/app.routes.ts
index 59ef4ce55..03e2bce51 100644
--- a/client/src/app/app.routes.ts
+++ b/client/src/app/app.routes.ts
@@ -1,15 +1,18 @@
1import { RouterConfig } from '@angular/router'; 1import { Routes } from '@angular/router';
2 2
3import { AccountRoutes } from './account';
3import { LoginRoutes } from './login'; 4import { LoginRoutes } from './login';
5import { AdminRoutes } from './admin';
4import { VideosRoutes } from './videos'; 6import { VideosRoutes } from './videos';
5 7
6export const routes: RouterConfig = [ 8export const routes: Routes = [
7 { 9 {
8 path: '', 10 path: '',
9 redirectTo: '/videos/list', 11 redirectTo: '/videos/list',
10 pathMatch: 'full' 12 pathMatch: 'full'
11 }, 13 },
12 14 ...AdminRoutes,
15 ...AccountRoutes,
13 ...LoginRoutes, 16 ...LoginRoutes,
14 ...VideosRoutes 17 ...VideosRoutes
15]; 18];
diff --git a/client/src/app/app.service.ts b/client/src/app/app.service.ts
new file mode 100644
index 000000000..033c21900
--- /dev/null
+++ b/client/src/app/app.service.ts
@@ -0,0 +1,36 @@
1
2import { Injectable } from '@angular/core';
3
4@Injectable()
5export class AppState {
6 _state = { };
7
8 constructor() { ; }
9
10 // already return a clone of the current state
11 get state() {
12 return this._state = this._clone(this._state);
13 }
14 // never allow mutation
15 set state(value) {
16 throw new Error('do not mutate the `.state` directly');
17 }
18
19
20 get(prop?: any) {
21 // use our state getter for the clone
22 const state = this.state;
23 return state.hasOwnProperty(prop) ? state[prop] : state;
24 }
25
26 set(prop: string, value: any) {
27 // internally mutate our state
28 return this._state[prop] = value;
29 }
30
31
32 _clone(object) {
33 // simple object clone
34 return JSON.parse(JSON.stringify( object ));
35 }
36}
diff --git a/client/src/app/environment.ts b/client/src/app/environment.ts
new file mode 100644
index 000000000..8bba89c4e
--- /dev/null
+++ b/client/src/app/environment.ts
@@ -0,0 +1,50 @@
1
2// Angular 2
3// rc2 workaround
4import { enableDebugTools, disableDebugTools } from '@angular/platform-browser';
5import { enableProdMode, ApplicationRef } from '@angular/core';
6// Environment Providers
7let PROVIDERS = [
8 // common env directives
9];
10
11// Angular debug tools in the dev console
12// https://github.com/angular/angular/blob/86405345b781a9dc2438c0fbe3e9409245647019/TOOLS_JS.md
13let _decorateModuleRef = function identity(value) { return value; };
14
15if ('production' === ENV) {
16 // Production
17 disableDebugTools();
18 enableProdMode();
19
20 PROVIDERS = [
21 ...PROVIDERS,
22 // custom providers in production
23 ];
24
25} else {
26
27 _decorateModuleRef = (modRef: any) => {
28 const appRef = modRef.injector.get(ApplicationRef);
29 const cmpRef = appRef.components[0];
30
31 let _ng = (<any>window).ng;
32 enableDebugTools(cmpRef);
33 (<any>window).ng.probe = _ng.probe;
34 (<any>window).ng.coreTokens = _ng.coreTokens;
35 return modRef;
36 };
37
38 // Development
39 PROVIDERS = [
40 ...PROVIDERS,
41 // custom providers in development
42 ];
43
44}
45
46export const decorateModuleRef = _decorateModuleRef;
47
48export const ENV_PROVIDERS = [
49 ...PROVIDERS
50];
diff --git a/client/src/app/friends/friend.service.ts b/client/src/app/friends/friend.service.ts
deleted file mode 100644
index 771046484..000000000
--- a/client/src/app/friends/friend.service.ts
+++ /dev/null
@@ -1,29 +0,0 @@
1import { Injectable } from '@angular/core';
2import { Response } from '@angular/http';
3import { Observable } from 'rxjs/Observable';
4
5import { AuthHttp, AuthService } from '../shared';
6
7@Injectable()
8export class FriendService {
9 private static BASE_FRIEND_URL: string = '/api/v1/pods/';
10
11 constructor (private authHttp: AuthHttp, private authService: AuthService) {}
12
13 makeFriends() {
14 return this.authHttp.get(FriendService.BASE_FRIEND_URL + 'makefriends')
15 .map(res => res.status)
16 .catch(this.handleError);
17 }
18
19 quitFriends() {
20 return this.authHttp.get(FriendService.BASE_FRIEND_URL + 'quitfriends')
21 .map(res => res.status)
22 .catch(this.handleError);
23 }
24
25 private handleError (error: Response): Observable<number> {
26 console.error(error);
27 return Observable.throw(error.json().error || 'Server error');
28 }
29}
diff --git a/client/src/app/index.ts b/client/src/app/index.ts
new file mode 100644
index 000000000..da53f6aef
--- /dev/null
+++ b/client/src/app/index.ts
@@ -0,0 +1 @@
export * from './app.module';
diff --git a/client/src/app/login/login.component.html b/client/src/app/login/login.component.html
index 5848fcba3..94a405405 100644
--- a/client/src/app/login/login.component.html
+++ b/client/src/app/login/login.component.html
@@ -1,17 +1,16 @@
1<h3>Login</h3> 1<h3>Login</h3>
2 2
3
4<div *ngIf="error" class="alert alert-danger">{{ error }}</div> 3<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
5 4
6<form role="form" (ngSubmit)="login(username.value, password.value)" #loginForm="ngForm"> 5<form role="form" (ngSubmit)="login()" [formGroup]="form">
7 <div class="form-group"> 6 <div class="form-group">
8 <label for="username">Username</label> 7 <label for="username">Username</label>
9 <input 8 <input
10 type="text" class="form-control" name="username" id="username" placeholder="Username" required 9 type="text" class="form-control" id="username" placeholder="Username" required
11 ngControl="username" #username="ngForm" 10 formControlName="username"
12 > 11 >
13 <div [hidden]="username.valid || username.pristine" class="alert alert-danger"> 12 <div *ngIf="formErrors.username" class="alert alert-danger">
14 Username is required 13 {{ formErrors.username }}
15 </div> 14 </div>
16 </div> 15 </div>
17 16
@@ -19,12 +18,12 @@
19 <label for="password">Password</label> 18 <label for="password">Password</label>
20 <input 19 <input
21 type="password" class="form-control" name="password" id="password" placeholder="Password" required 20 type="password" class="form-control" name="password" id="password" placeholder="Password" required
22 ngControl="password" #password="ngForm" 21 formControlName="password"
23 > 22 >
24 <div [hidden]="password.valid || password.pristine" class="alert alert-danger"> 23 <div *ngIf="formErrors.password" class="alert alert-danger">
25 Password is required 24 {{ formErrors.password }}
26 </div> 25 </div>
27 </div> 26 </div>
28 27
29 <input type="submit" value="Login" class="btn btn-default" [disabled]="!loginForm.form.valid"> 28 <input type="submit" value="Login" class="btn btn-default" [disabled]="!form.valid">
30</form> 29</form>
diff --git a/client/src/app/login/login.component.ts b/client/src/app/login/login.component.ts
index ddd62462e..c4ff7050b 100644
--- a/client/src/app/login/login.component.ts
+++ b/client/src/app/login/login.component.ts
@@ -1,35 +1,67 @@
1import { Component } from '@angular/core'; 1import { Component, OnInit } from '@angular/core';
2import { FormBuilder, FormGroup, Validators } from '@angular/forms';
2import { Router } from '@angular/router'; 3import { Router } from '@angular/router';
3 4
4import { AuthService } from '../shared'; 5import { AuthService, FormReactive } from '../shared';
5 6
6@Component({ 7@Component({
7 selector: 'my-login', 8 selector: 'my-login',
8 template: require('./login.component.html') 9 templateUrl: './login.component.html'
9}) 10})
10 11
11export class LoginComponent { 12export class LoginComponent extends FormReactive implements OnInit {
12 error: string = null; 13 error: string = null;
13 14
15 form: FormGroup;
16 formErrors = {
17 'username': '',
18 'password': ''
19 };
20 validationMessages = {
21 'username': {
22 'required': 'Username is required.',
23 },
24 'password': {
25 'required': 'Password is required.'
26 }
27 };
28
14 constructor( 29 constructor(
15 private authService: AuthService, 30 private authService: AuthService,
31 private formBuilder: FormBuilder,
16 private router: Router 32 private router: Router
17 ) {} 33 ) {
34 super();
35 }
36
37 buildForm() {
38 this.form = this.formBuilder.group({
39 username: [ '', Validators.required ],
40 password: [ '', Validators.required ],
41 });
42
43 this.form.valueChanges.subscribe(data => this.onValueChanged(data));
44 }
45
46 ngOnInit() {
47 this.buildForm();
48 }
49
50 login() {
51 this.error = null;
52
53 const { username, password } = this.form.value;
18 54
19 login(username: string, password: string) {
20 this.authService.login(username, password).subscribe( 55 this.authService.login(username, password).subscribe(
21 result => { 56 result => this.router.navigate(['/videos/list']),
22 this.error = null;
23 57
24 this.router.navigate(['/videos/list']);
25 },
26 error => { 58 error => {
27 console.error(error); 59 console.error(error.json);
28 60
29 if (error.error === 'invalid_grant') { 61 if (error.json.error === 'invalid_grant') {
30 this.error = 'Credentials are invalid.'; 62 this.error = 'Credentials are invalid.';
31 } else { 63 } else {
32 this.error = `${error.error}: ${error.error_description}`; 64 this.error = `${error.json.error}: ${error.json.error_description}`;
33 } 65 }
34 } 66 }
35 ); 67 );
diff --git a/client/src/app/menu.component.html b/client/src/app/menu.component.html
new file mode 100644
index 000000000..29ef7f9cf
--- /dev/null
+++ b/client/src/app/menu.component.html
@@ -0,0 +1,39 @@
1<menu class="col-md-2 col-sm-3 col-xs-3">
2 <div class="panel-block">
3 <div id="panel-user-login" class="panel-button">
4 <span *ngIf="!isLoggedIn" >
5 <span class="hidden-xs glyphicon glyphicon-log-in"></span>
6 <a [routerLink]="['/login']">Login</a>
7 </span>
8
9 <span *ngIf="isLoggedIn">
10 <span class="hidden-xs glyphicon glyphicon-log-out"></span>
11 <a *ngIf="isLoggedIn" (click)="logout()">Logout</a>
12 </span>
13 </div>
14
15 <div *ngIf="isLoggedIn" id="panel-user-account" class="panel-button">
16 <span class="hidden-xs glyphicon glyphicon-user"></span>
17 <a [routerLink]="['/account']">My account</a>
18 </div>
19 </div>
20
21 <div class="panel-block">
22 <div id="panel-get-videos" class="panel-button">
23 <span class="hidden-xs glyphicon glyphicon-list"></span>
24 <a [routerLink]="['/videos/list']">Get videos</a>
25 </div>
26
27 <div id="panel-upload-video" class="panel-button" *ngIf="isLoggedIn">
28 <span class="hidden-xs glyphicon glyphicon-cloud-upload"></span>
29 <a [routerLink]="['/videos/add']">Upload a video</a>
30 </div>
31 </div>
32
33 <div class="panel-block" *ngIf="isUserAdmin()">
34 <div id="panel-get-videos" class="panel-button">
35 <span class="hidden-xs glyphicon glyphicon-cog"></span>
36 <a [routerLink]="['/admin']">Administration</a>
37 </div>
38 </div>
39</menu>
diff --git a/client/src/app/menu.component.ts b/client/src/app/menu.component.ts
new file mode 100644
index 000000000..6cfc854dd
--- /dev/null
+++ b/client/src/app/menu.component.ts
@@ -0,0 +1,45 @@
1import { Component, OnInit } from '@angular/core';
2import { Router } from '@angular/router';
3
4import { AuthService, AuthStatus } from './shared';
5
6@Component({
7 selector: 'my-menu',
8 templateUrl: './menu.component.html'
9})
10export class MenuComponent implements OnInit {
11 isLoggedIn: boolean;
12
13 constructor (
14 private authService: AuthService,
15 private router: Router
16 ) {}
17
18 ngOnInit() {
19 this.isLoggedIn = this.authService.isLoggedIn();
20
21 this.authService.loginChangedSource.subscribe(
22 status => {
23 if (status === AuthStatus.LoggedIn) {
24 this.isLoggedIn = true;
25 console.log('Logged in.');
26 } else if (status === AuthStatus.LoggedOut) {
27 this.isLoggedIn = false;
28 console.log('Logged out.');
29 } else {
30 console.error('Unknown auth status: ' + status);
31 }
32 }
33 );
34 }
35
36 isUserAdmin() {
37 return this.authService.isAdmin();
38 }
39
40 logout() {
41 this.authService.logout();
42 // Redirect to home page
43 this.router.navigate(['/videos/list']);
44 }
45}
diff --git a/client/src/app/shared/auth/auth-http.service.ts b/client/src/app/shared/auth/auth-http.service.ts
index 9c7ef4389..2392898ca 100644
--- a/client/src/app/shared/auth/auth-http.service.ts
+++ b/client/src/app/shared/auth/auth-http.service.ts
@@ -28,7 +28,7 @@ export class AuthHttp extends Http {
28 return super.request(url, options) 28 return super.request(url, options)
29 .catch((err) => { 29 .catch((err) => {
30 if (err.status === 401) { 30 if (err.status === 401) {
31 return this.handleTokenExpired(err, url, options); 31 return this.handleTokenExpired(url, options);
32 } 32 }
33 33
34 return Observable.throw(err); 34 return Observable.throw(err);
@@ -49,26 +49,29 @@ export class AuthHttp extends Http {
49 return this.request(url, options); 49 return this.request(url, options);
50 } 50 }
51 51
52 post(url: string, options?: RequestOptionsArgs): Observable<Response> { 52 post(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
53 if (!options) options = {}; 53 if (!options) options = {};
54 options.method = RequestMethod.Post; 54 options.method = RequestMethod.Post;
55 options.body = body;
55 56
56 return this.request(url, options); 57 return this.request(url, options);
57 } 58 }
58 59
59 put(url: string, options?: RequestOptionsArgs): Observable<Response> { 60 put(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
60 if (!options) options = {}; 61 if (!options) options = {};
61 options.method = RequestMethod.Put; 62 options.method = RequestMethod.Put;
63 options.body = body;
62 64
63 return this.request(url, options); 65 return this.request(url, options);
64 } 66 }
65 67
66 private handleTokenExpired(err: Response, url: string | Request, options: RequestOptionsArgs) { 68 private handleTokenExpired(url: string | Request, options: RequestOptionsArgs) {
67 return this.authService.refreshAccessToken().flatMap(() => { 69 return this.authService.refreshAccessToken()
68 this.setAuthorizationHeader(options.headers); 70 .flatMap(() => {
71 this.setAuthorizationHeader(options.headers);
69 72
70 return super.request(url, options); 73 return super.request(url, options);
71 }); 74 });
72 } 75 }
73 76
74 private setAuthorizationHeader(headers: Headers) { 77 private setAuthorizationHeader(headers: Headers) {
diff --git a/client/src/app/shared/auth/user.model.ts b/client/src/app/shared/auth/auth-user.model.ts
index 98852f835..bdd5ea5a9 100644
--- a/client/src/app/shared/auth/user.model.ts
+++ b/client/src/app/shared/auth/auth-user.model.ts
@@ -1,15 +1,28 @@
1export class User { 1import { User } from '../users';
2
3export class AuthUser extends User {
2 private static KEYS = { 4 private static KEYS = {
5 ID: 'id',
6 ROLE: 'role',
3 USERNAME: 'username' 7 USERNAME: 'username'
4 }; 8 };
5 9
10 id: string;
11 role: string;
6 username: string; 12 username: string;
7 tokens: Tokens; 13 tokens: Tokens;
8 14
9 static load() { 15 static load() {
10 const usernameLocalStorage = localStorage.getItem(this.KEYS.USERNAME); 16 const usernameLocalStorage = localStorage.getItem(this.KEYS.USERNAME);
11 if (usernameLocalStorage) { 17 if (usernameLocalStorage) {
12 return new User(localStorage.getItem(this.KEYS.USERNAME), Tokens.load()); 18 return new AuthUser(
19 {
20 id: localStorage.getItem(this.KEYS.ID),
21 username: localStorage.getItem(this.KEYS.USERNAME),
22 role: localStorage.getItem(this.KEYS.ROLE)
23 },
24 Tokens.load()
25 );
13 } 26 }
14 27
15 return null; 28 return null;
@@ -17,12 +30,14 @@ export class User {
17 30
18 static flush() { 31 static flush() {
19 localStorage.removeItem(this.KEYS.USERNAME); 32 localStorage.removeItem(this.KEYS.USERNAME);
33 localStorage.removeItem(this.KEYS.ID);
34 localStorage.removeItem(this.KEYS.ROLE);
20 Tokens.flush(); 35 Tokens.flush();
21 } 36 }
22 37
23 constructor(username: string, hash_tokens: any) { 38 constructor(userHash: { id: string, username: string, role: string }, hashTokens: any) {
24 this.username = username; 39 super(userHash);
25 this.tokens = new Tokens(hash_tokens); 40 this.tokens = new Tokens(hashTokens);
26 } 41 }
27 42
28 getAccessToken() { 43 getAccessToken() {
@@ -43,12 +58,14 @@ export class User {
43 } 58 }
44 59
45 save() { 60 save() {
46 localStorage.setItem('username', this.username); 61 localStorage.setItem(AuthUser.KEYS.ID, this.id);
62 localStorage.setItem(AuthUser.KEYS.USERNAME, this.username);
63 localStorage.setItem(AuthUser.KEYS.ROLE, this.role);
47 this.tokens.save(); 64 this.tokens.save();
48 } 65 }
49} 66}
50 67
51// Private class used only by User 68// Private class only used by User
52class Tokens { 69class Tokens {
53 private static KEYS = { 70 private static KEYS = {
54 ACCESS_TOKEN: 'access_token', 71 ACCESS_TOKEN: 'access_token',
diff --git a/client/src/app/shared/auth/auth.service.ts b/client/src/app/shared/auth/auth.service.ts
index 584298fff..a30c79c86 100644
--- a/client/src/app/shared/auth/auth.service.ts
+++ b/client/src/app/shared/auth/auth.service.ts
@@ -1,32 +1,39 @@
1import { Injectable } from '@angular/core'; 1import { Injectable } from '@angular/core';
2import { Headers, Http, Response, URLSearchParams } from '@angular/http'; 2import { Headers, Http, Response, URLSearchParams } from '@angular/http';
3import { Router } from '@angular/router';
3import { Observable } from 'rxjs/Observable'; 4import { Observable } from 'rxjs/Observable';
4import { Subject } from 'rxjs/Subject'; 5import { Subject } from 'rxjs/Subject';
5 6
6import { AuthStatus } from './auth-status.model'; 7import { AuthStatus } from './auth-status.model';
7import { User } from './user.model'; 8import { AuthUser } from './auth-user.model';
9import { RestExtractor } from '../rest';
8 10
9@Injectable() 11@Injectable()
10export class AuthService { 12export class AuthService {
11 private static BASE_CLIENT_URL = '/api/v1/users/client'; 13 private static BASE_CLIENT_URL = '/api/v1/clients/local';
12 private static BASE_TOKEN_URL = '/api/v1/users/token'; 14 private static BASE_TOKEN_URL = '/api/v1/users/token';
15 private static BASE_USER_INFORMATIONS_URL = '/api/v1/users/me';
13 16
14 loginChangedSource: Observable<AuthStatus>; 17 loginChangedSource: Observable<AuthStatus>;
15 18
16 private clientId: string; 19 private clientId: string;
17 private clientSecret: string; 20 private clientSecret: string;
18 private loginChanged: Subject<AuthStatus>; 21 private loginChanged: Subject<AuthStatus>;
19 private user: User = null; 22 private user: AuthUser = null;
20 23
21 constructor(private http: Http) { 24 constructor(
25 private http: Http,
26 private restExtractor: RestExtractor,
27 private router: Router
28 ) {
22 this.loginChanged = new Subject<AuthStatus>(); 29 this.loginChanged = new Subject<AuthStatus>();
23 this.loginChangedSource = this.loginChanged.asObservable(); 30 this.loginChangedSource = this.loginChanged.asObservable();
24 31
25 // Fetch the client_id/client_secret 32 // Fetch the client_id/client_secret
26 // FIXME: save in local storage? 33 // FIXME: save in local storage?
27 this.http.get(AuthService.BASE_CLIENT_URL) 34 this.http.get(AuthService.BASE_CLIENT_URL)
28 .map(res => res.json()) 35 .map(this.restExtractor.extractDataGet)
29 .catch(this.handleError) 36 .catch((res) => this.restExtractor.handleError(res))
30 .subscribe( 37 .subscribe(
31 result => { 38 result => {
32 this.clientId = result.client_id; 39 this.clientId = result.client_id;
@@ -34,12 +41,15 @@ export class AuthService {
34 console.log('Client credentials loaded.'); 41 console.log('Client credentials loaded.');
35 }, 42 },
36 error => { 43 error => {
37 alert(error); 44 alert(
45 `Cannot retrieve OAuth Client credentials: ${error.text}. \n` +
46 'Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.'
47 );
38 } 48 }
39 ); 49 );
40 50
41 // Return null if there is nothing to load 51 // Return null if there is nothing to load
42 this.user = User.load(); 52 this.user = AuthUser.load();
43 } 53 }
44 54
45 getRefreshToken() { 55 getRefreshToken() {
@@ -64,10 +74,16 @@ export class AuthService {
64 return this.user.getTokenType(); 74 return this.user.getTokenType();
65 } 75 }
66 76
67 getUser(): User { 77 getUser(): AuthUser {
68 return this.user; 78 return this.user;
69 } 79 }
70 80
81 isAdmin() {
82 if (this.user === null) return false;
83
84 return this.user.isAdmin();
85 }
86
71 isLoggedIn() { 87 isLoggedIn() {
72 if (this.getAccessToken()) { 88 if (this.getAccessToken()) {
73 return true; 89 return true;
@@ -94,21 +110,23 @@ export class AuthService {
94 }; 110 };
95 111
96 return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options) 112 return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options)
97 .map(res => res.json()) 113 .map(this.restExtractor.extractDataGet)
98 .map(res => { 114 .map(res => {
99 res.username = username; 115 res.username = username;
100 return res; 116 return res;
101 }) 117 })
118 .flatMap(res => this.fetchUserInformations(res))
102 .map(res => this.handleLogin(res)) 119 .map(res => this.handleLogin(res))
103 .catch(this.handleError); 120 .catch((res) => this.restExtractor.handleError(res));
104 } 121 }
105 122
106 logout() { 123 logout() {
107 // TODO: make an HTTP request to revoke the tokens 124 // TODO: make an HTTP request to revoke the tokens
108 this.user = null; 125 this.user = null;
109 User.flush();
110 126
111 this.setStatus(AuthStatus.LoggedIn); 127 AuthUser.flush();
128
129 this.setStatus(AuthStatus.LoggedOut);
112 } 130 }
113 131
114 refreshAccessToken() { 132 refreshAccessToken() {
@@ -131,36 +149,64 @@ export class AuthService {
131 }; 149 };
132 150
133 return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options) 151 return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options)
134 .map(res => res.json()) 152 .map(this.restExtractor.extractDataGet)
135 .map(res => this.handleRefreshToken(res)) 153 .map(res => this.handleRefreshToken(res))
136 .catch(this.handleError); 154 .catch((res: Response) => {
155 // The refresh token is invalid?
156 if (res.status === 400 && res.json() && res.json().error === 'invalid_grant') {
157 console.error('Cannot refresh token -> logout...');
158 this.logout();
159 this.router.navigate(['/login']);
160
161 return Observable.throw({
162 json: '',
163 text: 'You need to reconnect.'
164 });
165 }
166
167 return this.restExtractor.handleError(res);
168 });
137 } 169 }
138 170
139 private setStatus(status: AuthStatus) { 171 private fetchUserInformations (obj: any) {
140 this.loginChanged.next(status); 172 // Do not call authHttp here to avoid circular dependencies headaches
173
174 const headers = new Headers();
175 headers.set('Authorization', `Bearer ${obj.access_token}`);
176
177 return this.http.get(AuthService.BASE_USER_INFORMATIONS_URL, { headers })
178 .map(res => res.json())
179 .map(res => {
180 obj.id = res.id;
181 obj.role = res.role;
182 return obj;
183 }
184 );
141 } 185 }
142 186
143 private handleLogin (obj: any) { 187 private handleLogin (obj: any) {
188 const id = obj.id;
144 const username = obj.username; 189 const username = obj.username;
145 const hash_tokens = { 190 const role = obj.role;
191 const hashTokens = {
146 access_token: obj.access_token, 192 access_token: obj.access_token,
147 token_type: obj.token_type, 193 token_type: obj.token_type,
148 refresh_token: obj.refresh_token 194 refresh_token: obj.refresh_token
149 }; 195 };
150 196
151 this.user = new User(username, hash_tokens); 197 this.user = new AuthUser({ id, username, role }, hashTokens);
152 this.user.save(); 198 this.user.save();
153 199
154 this.setStatus(AuthStatus.LoggedIn); 200 this.setStatus(AuthStatus.LoggedIn);
155 } 201 }
156 202
157 private handleError (error: Response) {
158 console.error(error);
159 return Observable.throw(error.json() || { error: 'Server error' });
160 }
161
162 private handleRefreshToken (obj: any) { 203 private handleRefreshToken (obj: any) {
163 this.user.refreshTokens(obj.access_token, obj.refresh_token); 204 this.user.refreshTokens(obj.access_token, obj.refresh_token);
164 this.user.save(); 205 this.user.save();
165 } 206 }
207
208 private setStatus(status: AuthStatus) {
209 this.loginChanged.next(status);
210 }
211
166} 212}
diff --git a/client/src/app/shared/auth/index.ts b/client/src/app/shared/auth/index.ts
index aafaacbf1..ebd9e14cd 100644
--- a/client/src/app/shared/auth/index.ts
+++ b/client/src/app/shared/auth/index.ts
@@ -1,4 +1,4 @@
1export * from './auth-http.service'; 1export * from './auth-http.service';
2export * from './auth-status.model'; 2export * from './auth-status.model';
3export * from './auth.service'; 3export * from './auth.service';
4export * from './user.model'; 4export * from './auth-user.model';
diff --git a/client/src/app/shared/forms/form-reactive.ts b/client/src/app/shared/forms/form-reactive.ts
new file mode 100644
index 000000000..1e8a69771
--- /dev/null
+++ b/client/src/app/shared/forms/form-reactive.ts
@@ -0,0 +1,24 @@
1import { FormGroup } from '@angular/forms';
2
3export abstract class FormReactive {
4 abstract form: FormGroup;
5 abstract formErrors: Object;
6 abstract validationMessages: Object;
7
8 abstract buildForm(): void;
9
10 protected onValueChanged(data?: any) {
11 for (const field in this.formErrors) {
12 // clear previous error message (if any)
13 this.formErrors[field] = '';
14 const control = this.form.get(field);
15
16 if (control && control.dirty && !control.valid) {
17 const messages = this.validationMessages[field];
18 for (const key in control.errors) {
19 this.formErrors[field] += messages[key] + ' ';
20 }
21 }
22 }
23 }
24}
diff --git a/client/src/app/shared/forms/form-validators/index.ts b/client/src/app/shared/forms/form-validators/index.ts
new file mode 100644
index 000000000..1d2ae6f68
--- /dev/null
+++ b/client/src/app/shared/forms/form-validators/index.ts
@@ -0,0 +1,3 @@
1export * from './url.validator';
2export * from './user';
3export * from './video';
diff --git a/client/src/app/shared/forms/form-validators/url.validator.ts b/client/src/app/shared/forms/form-validators/url.validator.ts
new file mode 100644
index 000000000..67163b4e9
--- /dev/null
+++ b/client/src/app/shared/forms/form-validators/url.validator.ts
@@ -0,0 +1,11 @@
1import { FormControl } from '@angular/forms';
2
3export function validateUrl(c: FormControl) {
4 let URL_REGEXP = new RegExp('^https?://(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$');
5
6 return URL_REGEXP.test(c.value) ? null : {
7 validateUrl: {
8 valid: false
9 }
10 };
11}
diff --git a/client/src/app/shared/forms/form-validators/user.ts b/client/src/app/shared/forms/form-validators/user.ts
new file mode 100644
index 000000000..5b11ff265
--- /dev/null
+++ b/client/src/app/shared/forms/form-validators/user.ts
@@ -0,0 +1,17 @@
1import { Validators } from '@angular/forms';
2
3export const USER_USERNAME = {
4 VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(20) ],
5 MESSAGES: {
6 'required': 'Username is required.',
7 'minlength': 'Username must be at least 3 characters long.',
8 'maxlength': 'Username cannot be more than 20 characters long.'
9 }
10};
11export const USER_PASSWORD = {
12 VALIDATORS: [ Validators.required, Validators.minLength(6) ],
13 MESSAGES: {
14 'required': 'Password is required.',
15 'minlength': 'Password must be at least 6 characters long.',
16 }
17};
diff --git a/client/src/app/shared/forms/form-validators/video.ts b/client/src/app/shared/forms/form-validators/video.ts
new file mode 100644
index 000000000..3766d4018
--- /dev/null
+++ b/client/src/app/shared/forms/form-validators/video.ts
@@ -0,0 +1,25 @@
1import { Validators } from '@angular/forms';
2
3export const VIDEO_NAME = {
4 VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(50) ],
5 MESSAGES: {
6 'required': 'Video name is required.',
7 'minlength': 'Video name must be at least 3 characters long.',
8 'maxlength': 'Video name cannot be more than 50 characters long.'
9 }
10};
11export const VIDEO_DESCRIPTION = {
12 VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(250) ],
13 MESSAGES: {
14 'required': 'Video description is required.',
15 'minlength': 'Video description must be at least 3 characters long.',
16 'maxlength': 'Video description cannot be more than 250 characters long.'
17 }
18};
19
20export const VIDEO_TAGS = {
21 VALIDATORS: [ Validators.pattern('^[a-zA-Z0-9]{2,10}$') ],
22 MESSAGES: {
23 'pattern': 'A tag should be between 2 and 10 alphanumeric characters long.'
24 }
25};
diff --git a/client/src/app/shared/forms/index.ts b/client/src/app/shared/forms/index.ts
new file mode 100644
index 000000000..588ebb4be
--- /dev/null
+++ b/client/src/app/shared/forms/index.ts
@@ -0,0 +1,2 @@
1export * from './form-validators';
2export * from './form-reactive';
diff --git a/client/src/app/shared/index.ts b/client/src/app/shared/index.ts
index dfea4c67c..af34b4b64 100644
--- a/client/src/app/shared/index.ts
+++ b/client/src/app/shared/index.ts
@@ -1,2 +1,5 @@
1export * from './auth'; 1export * from './auth';
2export * from './forms';
3export * from './rest';
2export * from './search'; 4export * from './search';
5export * from './users';
diff --git a/client/src/app/shared/rest/index.ts b/client/src/app/shared/rest/index.ts
new file mode 100644
index 000000000..3c9509dc7
--- /dev/null
+++ b/client/src/app/shared/rest/index.ts
@@ -0,0 +1,3 @@
1export * from './rest-extractor.service';
2export * from './rest-pagination';
3export * from './rest.service';
diff --git a/client/src/app/shared/rest/rest-extractor.service.ts b/client/src/app/shared/rest/rest-extractor.service.ts
new file mode 100644
index 000000000..fcb1598f4
--- /dev/null
+++ b/client/src/app/shared/rest/rest-extractor.service.ts
@@ -0,0 +1,52 @@
1import { Injectable } from '@angular/core';
2import { Response } from '@angular/http';
3import { Observable } from 'rxjs/Observable';
4
5export interface ResultList {
6 data: any[];
7 total: number;
8}
9
10@Injectable()
11export class RestExtractor {
12
13 constructor () { ; }
14
15 extractDataBool(res: Response) {
16 return true;
17 }
18
19 extractDataList(res: Response) {
20 const body = res.json();
21
22 const ret: ResultList = {
23 data: body.data,
24 total: body.total
25 };
26
27 return ret;
28 }
29
30 extractDataGet(res: Response) {
31 return res.json();
32 }
33
34 handleError(res: Response) {
35 let text = 'Server error: ';
36 text += res.text();
37 let json = '';
38
39 try {
40 json = res.json();
41 } catch (err) { ; }
42
43 const error = {
44 json,
45 text
46 };
47
48 console.error(error);
49
50 return Observable.throw(error);
51 }
52}
diff --git a/client/src/app/videos/shared/pagination.model.ts b/client/src/app/shared/rest/rest-pagination.ts
index eda44ebfb..0cfa4f468 100644
--- a/client/src/app/videos/shared/pagination.model.ts
+++ b/client/src/app/shared/rest/rest-pagination.ts
@@ -1,5 +1,5 @@
1export interface Pagination { 1export interface RestPagination {
2 currentPage: number; 2 currentPage: number;
3 itemsPerPage: number; 3 itemsPerPage: number;
4 totalItems: number; 4 totalItems: number;
5} 5};
diff --git a/client/src/app/shared/rest/rest.service.ts b/client/src/app/shared/rest/rest.service.ts
new file mode 100644
index 000000000..16b47e957
--- /dev/null
+++ b/client/src/app/shared/rest/rest.service.ts
@@ -0,0 +1,27 @@
1import { Injectable } from '@angular/core';
2import { URLSearchParams } from '@angular/http';
3
4import { RestPagination } from './rest-pagination';
5
6@Injectable()
7export class RestService {
8
9 buildRestGetParams(pagination?: RestPagination, sort?: string) {
10 const params = new URLSearchParams();
11
12 if (pagination) {
13 const start: number = (pagination.currentPage - 1) * pagination.itemsPerPage;
14 const count: number = pagination.itemsPerPage;
15
16 params.set('start', start.toString());
17 params.set('count', count.toString());
18 }
19
20 if (sort) {
21 params.set('sort', sort);
22 }
23
24 return params;
25 }
26
27}
diff --git a/client/src/app/shared/search/search.component.ts b/client/src/app/shared/search/search.component.ts
index e864fbc17..b6237469b 100644
--- a/client/src/app/shared/search/search.component.ts
+++ b/client/src/app/shared/search/search.component.ts
@@ -1,15 +1,13 @@
1import { Component, OnInit } from '@angular/core'; 1import { Component, OnInit } from '@angular/core';
2 2import { Router } from '@angular/router';
3import { DROPDOWN_DIRECTIVES} from 'ng2-bootstrap/components/dropdown';
4 3
5import { Search } from './search.model'; 4import { Search } from './search.model';
6import { SearchField } from './search-field.type'; 5import { SearchField } from './search-field.type';
7import { SearchService } from './search.service'; 6import { SearchService } from './search.service';
8 7
9@Component({ 8@Component({
10 selector: 'my-search', 9 selector: 'my-search',
11 template: require('./search.component.html'), 10 templateUrl: './search.component.html'
12 directives: [ DROPDOWN_DIRECTIVES ]
13}) 11})
14 12
15export class SearchComponent implements OnInit { 13export class SearchComponent implements OnInit {
@@ -25,10 +23,10 @@ export class SearchComponent implements OnInit {
25 value: '' 23 value: ''
26 }; 24 };
27 25
28 constructor(private searchService: SearchService) {} 26 constructor(private searchService: SearchService, private router: Router) {}
29 27
30 ngOnInit() { 28 ngOnInit() {
31 // Subscribe is the search changed 29 // Subscribe if the search changed
32 // Usually changed by videos list component 30 // Usually changed by videos list component
33 this.searchService.updateSearch.subscribe( 31 this.searchService.updateSearch.subscribe(
34 newSearchCriterias => { 32 newSearchCriterias => {
@@ -58,6 +56,10 @@ export class SearchComponent implements OnInit {
58 } 56 }
59 57
60 doSearch() { 58 doSearch() {
59 if (this.router.url.indexOf('/videos/list') === -1) {
60 this.router.navigate([ '/videos/list' ]);
61 }
62
61 this.searchService.searchUpdated.next(this.searchCriterias); 63 this.searchService.searchUpdated.next(this.searchCriterias);
62 } 64 }
63 65
diff --git a/client/src/app/shared/search/search.service.ts b/client/src/app/shared/search/search.service.ts
index c7993db3d..717a7fa50 100644
--- a/client/src/app/shared/search/search.service.ts
+++ b/client/src/app/shared/search/search.service.ts
@@ -1,5 +1,6 @@
1import { Injectable } from '@angular/core'; 1import { Injectable } from '@angular/core';
2import { Subject } from 'rxjs/Subject'; 2import { Subject } from 'rxjs/Subject';
3import { ReplaySubject } from 'rxjs/ReplaySubject';
3 4
4import { Search } from './search.model'; 5import { Search } from './search.model';
5 6
@@ -12,6 +13,6 @@ export class SearchService {
12 13
13 constructor() { 14 constructor() {
14 this.updateSearch = new Subject<Search>(); 15 this.updateSearch = new Subject<Search>();
15 this.searchUpdated = new Subject<Search>(); 16 this.searchUpdated = new ReplaySubject<Search>(1);
16 } 17 }
17} 18}
diff --git a/client/src/app/shared/users/index.ts b/client/src/app/shared/users/index.ts
new file mode 100644
index 000000000..5a670ce8f
--- /dev/null
+++ b/client/src/app/shared/users/index.ts
@@ -0,0 +1 @@
export * from './user.model';
diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts
new file mode 100644
index 000000000..726495d11
--- /dev/null
+++ b/client/src/app/shared/users/user.model.ts
@@ -0,0 +1,20 @@
1export class User {
2 id: string;
3 username: string;
4 role: string;
5 createdDate: Date;
6
7 constructor(hash: { id: string, username: string, role: string, createdDate?: Date }) {
8 this.id = hash.id;
9 this.username = hash.username;
10 this.role = hash.role;
11
12 if (hash.createdDate) {
13 this.createdDate = hash.createdDate;
14 }
15 }
16
17 isAdmin() {
18 return this.role === 'admin';
19 }
20}
diff --git a/client/src/app/videos/shared/index.ts b/client/src/app/videos/shared/index.ts
index a54120f5d..67d16ead1 100644
--- a/client/src/app/videos/shared/index.ts
+++ b/client/src/app/videos/shared/index.ts
@@ -1,5 +1,4 @@
1export * from './loader'; 1export * from './loader';
2export * from './pagination.model';
3export * from './sort-field.type'; 2export * from './sort-field.type';
4export * from './video.model'; 3export * from './video.model';
5export * from './video.service'; 4export * from './video.service';
diff --git a/client/src/app/videos/shared/loader/loader.component.ts b/client/src/app/videos/shared/loader/loader.component.ts
index cdd07d1b4..e72d2f3f3 100644
--- a/client/src/app/videos/shared/loader/loader.component.ts
+++ b/client/src/app/videos/shared/loader/loader.component.ts
@@ -2,8 +2,8 @@ import { Component, Input } from '@angular/core';
2 2
3@Component({ 3@Component({
4 selector: 'my-loader', 4 selector: 'my-loader',
5 styles: [ require('./loader.component.scss') ], 5 styleUrls: [ './loader.component.scss' ],
6 template: require('./loader.component.html') 6 templateUrl: './loader.component.html'
7}) 7})
8 8
9export class LoaderComponent { 9export class LoaderComponent {
diff --git a/client/src/app/videos/shared/video.service.ts b/client/src/app/videos/shared/video.service.ts
index b4396f767..ad8557533 100644
--- a/client/src/app/videos/shared/video.service.ts
+++ b/client/src/app/videos/shared/video.service.ts
@@ -1,11 +1,10 @@
1import { Injectable } from '@angular/core'; 1import { Injectable } from '@angular/core';
2import { Http, Response, URLSearchParams } from '@angular/http'; 2import { Http } from '@angular/http';
3import { Observable } from 'rxjs/Observable'; 3import { Observable } from 'rxjs/Observable';
4 4
5import { Pagination } from './pagination.model';
6import { Search } from '../../shared'; 5import { Search } from '../../shared';
7import { SortField } from './sort-field.type'; 6import { SortField } from './sort-field.type';
8import { AuthHttp, AuthService } from '../../shared'; 7import { AuthHttp, AuthService, RestExtractor, RestPagination, RestService, ResultList } from '../../shared';
9import { Video } from './video.model'; 8import { Video } from './video.model';
10 9
11@Injectable() 10@Injectable()
@@ -15,68 +14,51 @@ export class VideoService {
15 constructor( 14 constructor(
16 private authService: AuthService, 15 private authService: AuthService,
17 private authHttp: AuthHttp, 16 private authHttp: AuthHttp,
18 private http: Http 17 private http: Http,
18 private restExtractor: RestExtractor,
19 private restService: RestService
19 ) {} 20 ) {}
20 21
21 getVideo(id: string) { 22 getVideo(id: string): Observable<Video> {
22 return this.http.get(VideoService.BASE_VIDEO_URL + id) 23 return this.http.get(VideoService.BASE_VIDEO_URL + id)
23 .map(res => <Video> res.json()) 24 .map(this.restExtractor.extractDataGet)
24 .catch(this.handleError); 25 .catch((res) => this.restExtractor.handleError(res));
25 } 26 }
26 27
27 getVideos(pagination: Pagination, sort: SortField) { 28 getVideos(pagination: RestPagination, sort: SortField) {
28 const params = this.createPaginationParams(pagination); 29 const params = this.restService.buildRestGetParams(pagination, sort);
29
30 if (sort) params.set('sort', sort);
31 30
32 return this.http.get(VideoService.BASE_VIDEO_URL, { search: params }) 31 return this.http.get(VideoService.BASE_VIDEO_URL, { search: params })
33 .map(res => res.json()) 32 .map(res => res.json())
34 .map(this.extractVideos) 33 .map(this.extractVideos)
35 .catch(this.handleError); 34 .catch((res) => this.restExtractor.handleError(res));
36 } 35 }
37 36
38 removeVideo(id: string) { 37 removeVideo(id: string) {
39 return this.authHttp.delete(VideoService.BASE_VIDEO_URL + id) 38 return this.authHttp.delete(VideoService.BASE_VIDEO_URL + id)
40 .map(res => <number> res.status) 39 .map(this.restExtractor.extractDataBool)
41 .catch(this.handleError); 40 .catch((res) => this.restExtractor.handleError(res));
42 } 41 }
43 42
44 searchVideos(search: Search, pagination: Pagination, sort: SortField) { 43 searchVideos(search: Search, pagination: RestPagination, sort: SortField) {
45 const params = this.createPaginationParams(pagination); 44 const params = this.restService.buildRestGetParams(pagination, sort);
46 45
47 if (search.field) params.set('field', search.field); 46 if (search.field) params.set('field', search.field);
48 if (sort) params.set('sort', sort);
49 47
50 return this.http.get(VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value), { search: params }) 48 return this.http.get(VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value), { search: params })
51 .map(res => res.json()) 49 .map(this.restExtractor.extractDataList)
52 .map(this.extractVideos) 50 .map(this.extractVideos)
53 .catch(this.handleError); 51 .catch((res) => this.restExtractor.handleError(res));
54 }
55
56 private createPaginationParams(pagination: Pagination) {
57 const params = new URLSearchParams();
58 const start: number = (pagination.currentPage - 1) * pagination.itemsPerPage;
59 const count: number = pagination.itemsPerPage;
60
61 params.set('start', start.toString());
62 params.set('count', count.toString());
63
64 return params;
65 } 52 }
66 53
67 private extractVideos(body: any) { 54 private extractVideos(result: ResultList) {
68 const videos_json = body.data; 55 const videosJson = result.data;
69 const totalVideos = body.total; 56 const totalVideos = result.total;
70 const videos = []; 57 const videos = [];
71 for (const video_json of videos_json) { 58 for (const videoJson of videosJson) {
72 videos.push(new Video(video_json)); 59 videos.push(new Video(videoJson));
73 } 60 }
74 61
75 return { videos, totalVideos }; 62 return { videos, totalVideos };
76 } 63 }
77
78 private handleError(error: Response) {
79 console.error(error);
80 return Observable.throw(error.json().error || 'Server error');
81 }
82} 64}
diff --git a/client/src/app/videos/video-add/video-add.component.html b/client/src/app/videos/video-add/video-add.component.html
index bcd78c7cb..64320cae7 100644
--- a/client/src/app/videos/video-add/video-add.component.html
+++ b/client/src/app/videos/video-add/video-add.component.html
@@ -2,31 +2,31 @@
2 2
3<div *ngIf="error" class="alert alert-danger">{{ error }}</div> 3<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
4 4
5<form novalidate (ngSubmit)="upload()" [ngFormModel]="videoForm"> 5<form novalidate (ngSubmit)="upload()" [formGroup]="form">
6 <div class="form-group"> 6 <div class="form-group">
7 <label for="name">Name</label> 7 <label for="name">Name</label>
8 <input 8 <input
9 type="text" class="form-control" name="name" id="name" 9 type="text" class="form-control" id="name"
10 ngControl="name" #name="ngForm" [(ngModel)]="video.name" 10 formControlName="name"
11 > 11 >
12 <div [hidden]="name.valid || name.pristine" class="alert alert-warning"> 12 <div *ngIf="formErrors.name" class="alert alert-danger">
13 A name is required and should be between 3 and 50 characters long 13 {{ formErrors.name }}
14 </div> 14 </div>
15 </div> 15 </div>
16 16
17 <div class="form-group"> 17 <div class="form-group">
18 <label for="tags">Tags</label> 18 <label for="tags">Tags</label>
19 <input 19 <input
20 type="text" class="form-control" name="tags" id="tags" 20 type="text" class="form-control" id="currentTag"
21 ngControl="tags" #tags="ngForm" [disabled]="isTagsInputDisabled" (keyup)="onTagKeyPress($event)" [(ngModel)]="currentTag" 21 formControlName="currentTag" (keyup)="onTagKeyPress($event)"
22 > 22 >
23 <div [hidden]="tags.valid || tags.pristine" class="alert alert-warning"> 23 <div *ngIf="formErrors.currentTag" class="alert alert-danger">
24 A tag should be between 2 and 10 characters (alphanumeric) long 24 {{ formErrors.currentTag }}
25 </div> 25 </div>
26 </div> 26 </div>
27 27
28 <div class="tags"> 28 <div class="tags">
29 <div class="label label-primary tag" *ngFor="let tag of video.tags"> 29 <div class="label label-primary tag" *ngFor="let tag of tags">
30 {{ tag }} 30 {{ tag }}
31 <span class="remove" (click)="removeTag(tag)">x</span> 31 <span class="remove" (click)="removeTag(tag)">x</span>
32 </div> 32 </div>
@@ -53,12 +53,12 @@
53 <div class="form-group"> 53 <div class="form-group">
54 <label for="description">Description</label> 54 <label for="description">Description</label>
55 <textarea 55 <textarea
56 name="description" id="description" class="form-control" placeholder="Description..." 56 id="description" class="form-control" placeholder="Description..."
57 ngControl="description" #description="ngForm" [(ngModel)]="video.description" 57 formControlName="description"
58 > 58 >
59 </textarea> 59 </textarea>
60 <div [hidden]="description.valid || description.pristine" class="alert alert-warning"> 60 <div *ngIf="formErrors.description" class="alert alert-danger">
61 A description is required and should be between 3 and 250 characters long 61 {{ formErrors.description }}
62 </div> 62 </div>
63 </div> 63 </div>
64 64
@@ -69,7 +69,7 @@
69 <div class="form-group"> 69 <div class="form-group">
70 <input 70 <input
71 type="submit" value="Upload" class="btn btn-default form-control" [title]="getInvalidFieldsTitle()" 71 type="submit" value="Upload" class="btn btn-default form-control" [title]="getInvalidFieldsTitle()"
72 [disabled]="!videoForm.valid || video.tags.length === 0 || filename === null" 72 [disabled]="!form.valid || tags.length === 0 || filename === null"
73 > 73 >
74 </div> 74 </div>
75</form> 75</form>
diff --git a/client/src/app/videos/video-add/video-add.component.ts b/client/src/app/videos/video-add/video-add.component.ts
index c0f8cb9c4..d12a7d572 100644
--- a/client/src/app/videos/video-add/video-add.component.ts
+++ b/client/src/app/videos/video-add/video-add.component.ts
@@ -1,37 +1,42 @@
1import { Control, ControlGroup, Validators } from '@angular/common';
2import { Component, ElementRef, OnInit } from '@angular/core'; 1import { Component, ElementRef, OnInit } from '@angular/core';
2import { FormBuilder, FormGroup } from '@angular/forms';
3import { Router } from '@angular/router'; 3import { Router } from '@angular/router';
4 4
5import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe'; 5import { FileUploader } from 'ng2-file-upload/ng2-file-upload';
6import { PROGRESSBAR_DIRECTIVES } from 'ng2-bootstrap/components/progressbar';
7import { FileSelectDirective, FileUploader } from 'ng2-file-upload/ng2-file-upload';
8 6
9import { AuthService } from '../../shared'; 7import { AuthService, FormReactive, VIDEO_NAME, VIDEO_DESCRIPTION, VIDEO_TAGS } from '../../shared';
10 8
11@Component({ 9@Component({
12 selector: 'my-videos-add', 10 selector: 'my-videos-add',
13 styles: [ require('./video-add.component.scss') ], 11 styleUrls: [ './video-add.component.scss' ],
14 template: require('./video-add.component.html'), 12 templateUrl: './video-add.component.html'
15 directives: [ FileSelectDirective, PROGRESSBAR_DIRECTIVES ],
16 pipes: [ BytesPipe ]
17}) 13})
18 14
19export class VideoAddComponent implements OnInit { 15export class VideoAddComponent extends FormReactive implements OnInit {
20 currentTag: string; // Tag the user is writing in the input 16 tags: string[] = [];
21 error: string = null;
22 videoForm: ControlGroup;
23 uploader: FileUploader; 17 uploader: FileUploader;
24 video = { 18
19 error: string = null;
20 form: FormGroup;
21 formErrors = {
25 name: '', 22 name: '',
26 tags: [], 23 description: '',
27 description: '' 24 currentTag: ''
25 };
26 validationMessages = {
27 name: VIDEO_NAME.MESSAGES,
28 description: VIDEO_DESCRIPTION.MESSAGES,
29 currentTag: VIDEO_TAGS.MESSAGES
28 }; 30 };
29 31
30 constructor( 32 constructor(
31 private authService: AuthService, 33 private authService: AuthService,
32 private elementRef: ElementRef, 34 private elementRef: ElementRef,
35 private formBuilder: FormBuilder,
33 private router: Router 36 private router: Router
34 ) {} 37 ) {
38 super();
39 }
35 40
36 get filename() { 41 get filename() {
37 if (this.uploader.queue.length === 0) { 42 if (this.uploader.queue.length === 0) {
@@ -41,20 +46,26 @@ export class VideoAddComponent implements OnInit {
41 return this.uploader.queue[0].file.name; 46 return this.uploader.queue[0].file.name;
42 } 47 }
43 48
44 get isTagsInputDisabled () { 49 buildForm() {
45 return this.video.tags.length >= 3; 50 this.form = this.formBuilder.group({
51 name: [ '', VIDEO_NAME.VALIDATORS ],
52 description: [ '', VIDEO_DESCRIPTION.VALIDATORS ],
53 currentTag: [ '', VIDEO_TAGS.VALIDATORS ]
54 });
55
56 this.form.valueChanges.subscribe(data => this.onValueChanged(data));
46 } 57 }
47 58
48 getInvalidFieldsTitle() { 59 getInvalidFieldsTitle() {
49 let title = ''; 60 let title = '';
50 const nameControl = this.videoForm.controls['name']; 61 const nameControl = this.form.controls['name'];
51 const descriptionControl = this.videoForm.controls['description']; 62 const descriptionControl = this.form.controls['description'];
52 63
53 if (!nameControl.valid) { 64 if (!nameControl.valid) {
54 title += 'A name is required\n'; 65 title += 'A name is required\n';
55 } 66 }
56 67
57 if (this.video.tags.length === 0) { 68 if (this.tags.length === 0) {
58 title += 'At least one tag is required\n'; 69 title += 'At least one tag is required\n';
59 } 70 }
60 71
@@ -70,13 +81,6 @@ export class VideoAddComponent implements OnInit {
70 } 81 }
71 82
72 ngOnInit() { 83 ngOnInit() {
73 this.videoForm = new ControlGroup({
74 name: new Control('', Validators.compose([ Validators.required, Validators.minLength(3), Validators.maxLength(50) ])),
75 description: new Control('', Validators.compose([ Validators.required, Validators.minLength(3), Validators.maxLength(250) ])),
76 tags: new Control('', Validators.pattern('^[a-zA-Z0-9]{2,10}$'))
77 });
78
79
80 this.uploader = new FileUploader({ 84 this.uploader = new FileUploader({
81 authToken: this.authService.getRequestHeaderValue(), 85 authToken: this.authService.getRequestHeaderValue(),
82 queueLimit: 1, 86 queueLimit: 1,
@@ -85,26 +89,37 @@ export class VideoAddComponent implements OnInit {
85 }); 89 });
86 90
87 this.uploader.onBuildItemForm = (item, form) => { 91 this.uploader.onBuildItemForm = (item, form) => {
88 form.append('name', this.video.name); 92 const name = this.form.value['name'];
89 form.append('description', this.video.description); 93 const description = this.form.value['description'];
94
95 form.append('name', name);
96 form.append('description', description);
90 97
91 for (let i = 0; i < this.video.tags.length; i++) { 98 for (let i = 0; i < this.tags.length; i++) {
92 form.append(`tags[${i}]`, this.video.tags[i]); 99 form.append(`tags[${i}]`, this.tags[i]);
93 } 100 }
94 }; 101 };
102
103 this.buildForm();
95 } 104 }
96 105
97 onTagKeyPress(event: KeyboardEvent) { 106 onTagKeyPress(event: KeyboardEvent) {
107 const currentTag = this.form.value['currentTag'];
108
98 // Enter press 109 // Enter press
99 if (event.keyCode === 13) { 110 if (event.keyCode === 13) {
100 // Check if the tag is valid and does not already exist 111 // Check if the tag is valid and does not already exist
101 if ( 112 if (
102 this.currentTag !== '' && 113 currentTag !== '' &&
103 this.videoForm.controls['tags'].valid && 114 this.form.controls['currentTag'].valid &&
104 this.video.tags.indexOf(this.currentTag) === -1 115 this.tags.indexOf(currentTag) === -1
105 ) { 116 ) {
106 this.video.tags.push(this.currentTag); 117 this.tags.push(currentTag);
107 this.currentTag = ''; 118 this.form.patchValue({ currentTag: '' });
119
120 if (this.tags.length >= 3) {
121 this.form.get('currentTag').disable();
122 }
108 } 123 }
109 } 124 }
110 } 125 }
@@ -114,7 +129,7 @@ export class VideoAddComponent implements OnInit {
114 } 129 }
115 130
116 removeTag(tag: string) { 131 removeTag(tag: string) {
117 this.video.tags.splice(this.video.tags.indexOf(tag), 1); 132 this.tags.splice(this.tags.indexOf(tag), 1);
118 } 133 }
119 134
120 upload() { 135 upload() {
diff --git a/client/src/app/videos/video-list/video-list.component.ts b/client/src/app/videos/video-list/video-list.component.ts
index 5691d684e..6b086e938 100644
--- a/client/src/app/videos/video-list/video-list.component.ts
+++ b/client/src/app/videos/video-list/video-list.component.ts
@@ -1,39 +1,30 @@
1import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; 1import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
2import { AsyncPipe } from '@angular/common'; 2import { ActivatedRoute, Router } from '@angular/router';
3import { ActivatedRoute, Router, ROUTER_DIRECTIVES } from '@angular/router';
4import { BehaviorSubject } from 'rxjs/BehaviorSubject'; 3import { BehaviorSubject } from 'rxjs/BehaviorSubject';
5 4
6import { PAGINATION_DIRECTIVES } from 'ng2-bootstrap/components/pagination';
7
8import { 5import {
9 LoaderComponent,
10 Pagination,
11 SortField, 6 SortField,
12 Video, 7 Video,
13 VideoService 8 VideoService
14} from '../shared'; 9} from '../shared';
15import { AuthService, Search, SearchField, User } from '../../shared'; 10import { AuthService, AuthUser, RestPagination, Search, SearchField } from '../../shared';
16import { VideoMiniatureComponent } from './video-miniature.component';
17import { VideoSortComponent } from './video-sort.component';
18import { SearchService } from '../../shared'; 11import { SearchService } from '../../shared';
19 12
20@Component({ 13@Component({
21 selector: 'my-videos-list', 14 selector: 'my-videos-list',
22 styles: [ require('./video-list.component.scss') ], 15 styleUrls: [ './video-list.component.scss' ],
23 pipes: [ AsyncPipe ], 16 templateUrl: './video-list.component.html'
24 template: require('./video-list.component.html'),
25 directives: [ LoaderComponent, PAGINATION_DIRECTIVES, ROUTER_DIRECTIVES, VideoMiniatureComponent, VideoSortComponent ]
26}) 17})
27 18
28export class VideoListComponent implements OnInit, OnDestroy { 19export class VideoListComponent implements OnInit, OnDestroy {
29 loading: BehaviorSubject<boolean> = new BehaviorSubject(false); 20 loading: BehaviorSubject<boolean> = new BehaviorSubject(false);
30 pagination: Pagination = { 21 pagination: RestPagination = {
31 currentPage: 1, 22 currentPage: 1,
32 itemsPerPage: 9, 23 itemsPerPage: 9,
33 totalItems: null 24 totalItems: null
34 }; 25 };
35 sort: SortField; 26 sort: SortField;
36 user: User = null; 27 user: AuthUser = null;
37 videos: Video[] = []; 28 videos: Video[] = [];
38 29
39 private search: Search; 30 private search: Search;
@@ -51,7 +42,7 @@ export class VideoListComponent implements OnInit, OnDestroy {
51 42
52 ngOnInit() { 43 ngOnInit() {
53 if (this.authService.isLoggedIn()) { 44 if (this.authService.isLoggedIn()) {
54 this.user = User.load(); 45 this.user = AuthUser.load();
55 } 46 }
56 47
57 // Subscribe to route changes 48 // Subscribe to route changes
@@ -66,6 +57,8 @@ export class VideoListComponent implements OnInit, OnDestroy {
66 // Subscribe to search changes 57 // Subscribe to search changes
67 this.subSearch = this.searchService.searchUpdated.subscribe(search => { 58 this.subSearch = this.searchService.searchUpdated.subscribe(search => {
68 this.search = search; 59 this.search = search;
60 // Reset pagination
61 this.pagination.currentPage = 1;
69 62
70 this.navigateToNewParams(); 63 this.navigateToNewParams();
71 }); 64 });
@@ -76,7 +69,7 @@ export class VideoListComponent implements OnInit, OnDestroy {
76 this.subSearch.unsubscribe(); 69 this.subSearch.unsubscribe();
77 } 70 }
78 71
79 getVideos(detectChanges = true) { 72 getVideos() {
80 this.loading.next(true); 73 this.loading.next(true);
81 this.videos = []; 74 this.videos = [];
82 75
@@ -97,7 +90,7 @@ export class VideoListComponent implements OnInit, OnDestroy {
97 90
98 this.loading.next(false); 91 this.loading.next(false);
99 }, 92 },
100 error => alert(error) 93 error => alert(error.text)
101 ); 94 );
102 } 95 }
103 96
@@ -153,7 +146,11 @@ export class VideoListComponent implements OnInit, OnDestroy {
153 146
154 this.sort = <SortField>routeParams['sort'] || '-createdDate'; 147 this.sort = <SortField>routeParams['sort'] || '-createdDate';
155 148
156 this.pagination.currentPage = parseInt(routeParams['page']) || 1; 149 if (routeParams['page'] !== undefined) {
150 this.pagination.currentPage = parseInt(routeParams['page']);
151 } else {
152 this.pagination.currentPage = 1;
153 }
157 154
158 this.changeDetector.detectChanges(); 155 this.changeDetector.detectChanges();
159 } 156 }
diff --git a/client/src/app/videos/video-list/video-miniature.component.ts b/client/src/app/videos/video-list/video-miniature.component.ts
index 84bab950e..398d2db75 100644
--- a/client/src/app/videos/video-list/video-miniature.component.ts
+++ b/client/src/app/videos/video-list/video-miniature.component.ts
@@ -1,16 +1,12 @@
1import { DatePipe } from '@angular/common';
2import { Component, Input, Output, EventEmitter } from '@angular/core'; 1import { Component, Input, Output, EventEmitter } from '@angular/core';
3import { ROUTER_DIRECTIVES } from '@angular/router';
4 2
5import { SortField, Video, VideoService } from '../shared'; 3import { SortField, Video, VideoService } from '../shared';
6import { User } from '../../shared'; 4import { User } from '../../shared';
7 5
8@Component({ 6@Component({
9 selector: 'my-video-miniature', 7 selector: 'my-video-miniature',
10 styles: [ require('./video-miniature.component.scss') ], 8 styleUrls: [ './video-miniature.component.scss' ],
11 template: require('./video-miniature.component.html'), 9 templateUrl: './video-miniature.component.html'
12 directives: [ ROUTER_DIRECTIVES ],
13 pipes: [ DatePipe ]
14}) 10})
15 11
16export class VideoMiniatureComponent { 12export class VideoMiniatureComponent {
@@ -40,7 +36,7 @@ export class VideoMiniatureComponent {
40 if (confirm('Do you really want to remove this video?')) { 36 if (confirm('Do you really want to remove this video?')) {
41 this.videoService.removeVideo(id).subscribe( 37 this.videoService.removeVideo(id).subscribe(
42 status => this.removed.emit(true), 38 status => this.removed.emit(true),
43 error => alert(error) 39 error => alert(error.text)
44 ); 40 );
45 } 41 }
46 } 42 }
diff --git a/client/src/app/videos/video-list/video-sort.component.ts b/client/src/app/videos/video-list/video-sort.component.ts
index 0d76b54b7..ca94b07c2 100644
--- a/client/src/app/videos/video-list/video-sort.component.ts
+++ b/client/src/app/videos/video-list/video-sort.component.ts
@@ -4,7 +4,7 @@ import { SortField } from '../shared';
4 4
5@Component({ 5@Component({
6 selector: 'my-video-sort', 6 selector: 'my-video-sort',
7 template: require('./video-sort.component.html') 7 templateUrl: './video-sort.component.html'
8}) 8})
9 9
10export class VideoSortComponent { 10export class VideoSortComponent {
diff --git a/client/src/app/videos/video-watch/video-watch.component.ts b/client/src/app/videos/video-watch/video-watch.component.ts
index 3aaed0487..239e24c99 100644
--- a/client/src/app/videos/video-watch/video-watch.component.ts
+++ b/client/src/app/videos/video-watch/video-watch.component.ts
@@ -1,18 +1,13 @@
1import { Component, ElementRef, OnDestroy, OnInit } from '@angular/core'; 1import { Component, ElementRef, NgZone, OnDestroy, OnInit } from '@angular/core';
2import { ActivatedRoute } from '@angular/router'; 2import { ActivatedRoute } from '@angular/router';
3 3
4import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe'; 4import { Video, VideoService } from '../shared';
5
6import { LoaderComponent, Video, VideoService } from '../shared';
7import { WebTorrentService } from './webtorrent.service'; 5import { WebTorrentService } from './webtorrent.service';
8 6
9@Component({ 7@Component({
10 selector: 'my-video-watch', 8 selector: 'my-video-watch',
11 template: require('./video-watch.component.html'), 9 templateUrl: './video-watch.component.html',
12 styles: [ require('./video-watch.component.scss') ], 10 styleUrls: [ './video-watch.component.scss' ]
13 providers: [ WebTorrentService ],
14 directives: [ LoaderComponent ],
15 pipes: [ BytesPipe ]
16}) 11})
17 12
18export class VideoWatchComponent implements OnInit, OnDestroy { 13export class VideoWatchComponent implements OnInit, OnDestroy {
@@ -31,6 +26,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
31 26
32 constructor( 27 constructor(
33 private elementRef: ElementRef, 28 private elementRef: ElementRef,
29 private ngZone: NgZone,
34 private route: ActivatedRoute, 30 private route: ActivatedRoute,
35 private videoService: VideoService, 31 private videoService: VideoService,
36 private webTorrentService: WebTorrentService 32 private webTorrentService: WebTorrentService
@@ -65,12 +61,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
65 } 61 }
66 }); 62 });
67 63
68 // Refresh each second 64 this.runInProgress(torrent);
69 this.torrentInfosInterval = setInterval(() => {
70 this.downloadSpeed = torrent.downloadSpeed;
71 this.numPeers = torrent.numPeers;
72 this.uploadSpeed = torrent.uploadSpeed;
73 }, 1000);
74 }); 65 });
75 } 66 }
76 67
@@ -91,7 +82,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
91 this.video = video; 82 this.video = video;
92 this.loadVideo(); 83 this.loadVideo();
93 }, 84 },
94 error => alert(error) 85 error => alert(error.text)
95 ); 86 );
96 }); 87 });
97 } 88 }
@@ -100,4 +91,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
100 this.error = true; 91 this.error = true;
101 console.error('The video load seems to be abnormally long.'); 92 console.error('The video load seems to be abnormally long.');
102 } 93 }
94
95 private runInProgress(torrent: any) {
96 // Refresh each second
97 this.torrentInfosInterval = setInterval(() => {
98 this.ngZone.run(() => {
99 this.downloadSpeed = torrent.downloadSpeed;
100 this.numPeers = torrent.numPeers;
101 this.uploadSpeed = torrent.uploadSpeed;
102 });
103 }, 1000);
104 }
103} 105}
diff --git a/client/src/app/videos/videos.component.ts b/client/src/app/videos/videos.component.ts
index 76252afbb..591e7523d 100644
--- a/client/src/app/videos/videos.component.ts
+++ b/client/src/app/videos/videos.component.ts
@@ -1,9 +1,7 @@
1import { Component } from '@angular/core'; 1import { Component } from '@angular/core';
2import { ROUTER_DIRECTIVES } from '@angular/router';
3 2
4@Component({ 3@Component({
5 template: '<router-outlet></router-outlet>', 4 template: '<router-outlet></router-outlet>'
6 directives: [ ROUTER_DIRECTIVES ]
7}) 5})
8 6
9export class VideosComponent { 7export class VideosComponent {
diff --git a/client/src/app/videos/videos.routes.ts b/client/src/app/videos/videos.routes.ts
index 1f088b376..074f96596 100644
--- a/client/src/app/videos/videos.routes.ts
+++ b/client/src/app/videos/videos.routes.ts
@@ -1,11 +1,11 @@
1import { RouterConfig } from '@angular/router'; 1import { Routes } from '@angular/router';
2 2
3import { VideoAddComponent } from './video-add'; 3import { VideoAddComponent } from './video-add';
4import { VideoListComponent } from './video-list'; 4import { VideoListComponent } from './video-list';
5import { VideosComponent } from './videos.component'; 5import { VideosComponent } from './videos.component';
6import { VideoWatchComponent } from './video-watch'; 6import { VideoWatchComponent } from './video-watch';
7 7
8export const VideosRoutes: RouterConfig = [ 8export const VideosRoutes: Routes = [
9 { 9 {
10 path: 'videos', 10 path: 'videos',
11 component: VideosComponent, 11 component: VideosComponent,
diff --git a/client/src/custom-typings.d.ts b/client/src/custom-typings.d.ts
index 14c7d8ade..95787181f 100644
--- a/client/src/custom-typings.d.ts
+++ b/client/src/custom-typings.d.ts
@@ -1,15 +1,27 @@
1/* 1/*
2 * Custom Type Definitions 2 * Custom Type Definitions
3 * When including 3rd party modules you also need to include the type definition for the module 3 * When including 3rd party modules you also need to include the type definition for the module
4 * if they don't provide one within the module. You can try to install it with typings 4 * if they don't provide one within the module. You can try to install it with @types
5 5
6typings install node --save 6npm install @types/node
7npm install @types/lodash
7 8
8 * If you can't find the type definition in the registry we can make an ambient definition in 9 * If you can't find the type definition in the registry we can make an ambient/global definition in
9 * this file for now. For example 10 * this file for now. For example
10 11
11declare module "my-module" { 12declare module 'my-module' {
12 export function doesSomething(value: string): string; 13 export function doesSomething(value: string): string;
14}
15
16 * If you are using a CommonJS module that is using module.exports then you will have to write your
17 * types using export = yourObjectOrFunction with a namespace above it
18 * notice how we have to create a namespace that is equal to the function we're
19 * assigning the export to
20
21declare module 'jwt-decode' {
22 function jwtDecode(token: string): any;
23 namespace jwtDecode {}
24 export = jwtDecode;
13} 25}
14 26
15 * 27 *
@@ -17,33 +29,65 @@ declare module "my-module" {
17 * 29 *
18 30
19declare var assert: any; 31declare var assert: any;
32declare var _: any;
33declare var $: any;
20 34
21 * 35 *
22 * If you're importing a module that uses Node.js modules which are CommonJS you need to import as 36 * If you're importing a module that uses Node.js modules which are CommonJS you need to import as
37 * in the files such as main.browser.ts or any file within app/
23 * 38 *
24 39
25import * as _ from 'lodash' 40import * as _ from 'lodash'
26 41
27 * You can include your type definitions in this file until you create one for the typings registry 42 * You can include your type definitions in this file until you create one for the @types
28 * see https://github.com/typings/registry
29 * 43 *
30 */ 44 */
31 45
46// support NodeJS modules without type definitions
47declare module '*';
32 48
33// Extra variables that live on Global that will be replaced by webpack DefinePlugin 49// Extra variables that live on Global that will be replaced by webpack DefinePlugin
34declare var ENV: string; 50declare var ENV: string;
35declare var HMR: boolean; 51declare var HMR: boolean;
52declare var System: SystemJS;
53
54interface SystemJS {
55 import: (path?: string) => Promise<any>;
56}
57
36interface GlobalEnvironment { 58interface GlobalEnvironment {
37 ENV; 59 ENV;
38 HMR; 60 HMR;
61 SystemJS: SystemJS;
62 System: SystemJS;
39} 63}
40 64
65interface Es6PromiseLoader {
66 (id: string): (exportName?: string) => Promise<any>;
67}
68
69type FactoryEs6PromiseLoader = () => Es6PromiseLoader;
70type FactoryPromise = () => Promise<any>;
71
72type AsyncRoutes = {
73 [component: string]: Es6PromiseLoader |
74 Function |
75 FactoryEs6PromiseLoader |
76 FactoryPromise
77};
78
79
80type IdleCallbacks = Es6PromiseLoader |
81 Function |
82 FactoryEs6PromiseLoader |
83 FactoryPromise ;
84
41interface WebpackModule { 85interface WebpackModule {
42 hot: { 86 hot: {
43 data?: any, 87 data?: any,
44 idle: any, 88 idle: any,
45 accept(dependencies?: string | string[], callback?: (updatedDependencies?: any) => void): void; 89 accept(dependencies?: string | string[], callback?: (updatedDependencies?: any) => void): void;
46 decline(dependencies?: string | string[]): void; 90 decline(deps?: any | string | string[]): void;
47 dispose(callback?: (data?: any) => void): void; 91 dispose(callback?: (data?: any) => void): void;
48 addDisposeHandler(callback?: (data?: any) => void): void; 92 addDisposeHandler(callback?: (data?: any) => void): void;
49 removeDisposeHandler(callback?: (data?: any) => void): void; 93 removeDisposeHandler(callback?: (data?: any) => void): void;
@@ -54,66 +98,26 @@ interface WebpackModule {
54 }; 98 };
55} 99}
56 100
101
57interface WebpackRequire { 102interface WebpackRequire {
58 context(file: string, flag?: boolean, exp?: RegExp): any; 103 (id: string): any;
104 (paths: string[], callback: (...modules: any[]) => void): void;
105 ensure(ids: string[], callback: (req: WebpackRequire) => void, chunkName?: string): void;
106 context(directory: string, useSubDirectories?: boolean, regExp?: RegExp): WebpackContext;
59} 107}
60 108
109interface WebpackContext extends WebpackRequire {
110 keys(): string[];
111}
61 112
62interface ErrorStackTraceLimit { 113interface ErrorStackTraceLimit {
63 stackTraceLimit: number; 114 stackTraceLimit: number;
64} 115}
65 116
66 117
67
68// Extend typings 118// Extend typings
69interface NodeRequire extends WebpackRequire {} 119interface NodeRequire extends WebpackRequire {}
70interface ErrorConstructor extends ErrorStackTraceLimit {} 120interface ErrorConstructor extends ErrorStackTraceLimit {}
121interface NodeRequireFunction extends Es6PromiseLoader {}
71interface NodeModule extends WebpackModule {} 122interface NodeModule extends WebpackModule {}
72interface Global extends GlobalEnvironment {} 123interface Global extends GlobalEnvironment {}
73
74
75declare namespace Reflect {
76 function decorate(decorators: ClassDecorator[], target: Function): Function;
77 function decorate(
78 decorators: (PropertyDecorator | MethodDecorator)[],
79 target: Object,
80 targetKey: string | symbol,
81 descriptor?: PropertyDescriptor): PropertyDescriptor;
82
83 function metadata(metadataKey: any, metadataValue: any): {
84 (target: Function): void;
85 (target: Object, propertyKey: string | symbol): void;
86 };
87 function defineMetadata(metadataKey: any, metadataValue: any, target: Object): void;
88 function defineMetadata(
89 metadataKey: any,
90 metadataValue: any,
91 target: Object,
92 targetKey: string | symbol): void;
93 function hasMetadata(metadataKey: any, target: Object): boolean;
94 function hasMetadata(metadataKey: any, target: Object, targetKey: string | symbol): boolean;
95 function hasOwnMetadata(metadataKey: any, target: Object): boolean;
96 function hasOwnMetadata(metadataKey: any, target: Object, targetKey: string | symbol): boolean;
97 function getMetadata(metadataKey: any, target: Object): any;
98 function getMetadata(metadataKey: any, target: Object, targetKey: string | symbol): any;
99 function getOwnMetadata(metadataKey: any, target: Object): any;
100 function getOwnMetadata(metadataKey: any, target: Object, targetKey: string | symbol): any;
101 function getMetadataKeys(target: Object): any[];
102 function getMetadataKeys(target: Object, targetKey: string | symbol): any[];
103 function getOwnMetadataKeys(target: Object): any[];
104 function getOwnMetadataKeys(target: Object, targetKey: string | symbol): any[];
105 function deleteMetadata(metadataKey: any, target: Object): boolean;
106 function deleteMetadata(metadataKey: any, target: Object, targetKey: string | symbol): boolean;
107}
108
109
110// We need this here since there is a problem with Zone.js typings
111interface Thenable<T> {
112 then<U>(
113 onFulfilled?: (value: T) => U | Thenable<U>,
114 onRejected?: (error: any) => U | Thenable<U>): Thenable<U>;
115 then<U>(
116 onFulfilled?: (value: T) => U | Thenable<U>,
117 onRejected?: (error: any) => void): Thenable<U>;
118 catch<U>(onRejected?: (error: any) => U | Thenable<U>): Thenable<U>;
119}
diff --git a/client/src/index.html b/client/src/index.html
index 5cf491221..f39d8d2cf 100644
--- a/client/src/index.html
+++ b/client/src/index.html
@@ -1,3 +1,4 @@
1<!DOCTYPE html>
1<html> 2<html>
2 <head> 3 <head>
3 <base href="/"> 4 <base href="/">
diff --git a/client/src/main.ts b/client/src/main.ts
index a78d275ad..70bf48537 100644
--- a/client/src/main.ts
+++ b/client/src/main.ts
@@ -1,28 +1,20 @@
1import { enableProdMode, provide } from '@angular/core'; 1import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
2import { 2import { decorateModuleRef } from './app/environment';
3 HTTP_PROVIDERS, 3import { bootloader } from '@angularclass/hmr';
4 RequestOptions, 4/*
5 XHRBackend 5 * App Module
6} from '@angular/http'; 6 * our top level module that holds all of our components
7import { bootstrap } from '@angular/platform-browser-dynamic'; 7 */
8import { provideRouter } from '@angular/router'; 8import { AppModule } from './app';
9 9
10import { AppComponent } from './app/app.component'; 10/*
11import { routes } from './app/app.routes'; 11 * Bootstrap our Angular app with a top level NgModule
12import { AuthHttp, AuthService } from './app/shared'; 12 */
13 13export function main(): Promise<any> {
14if (process.env.ENV === 'production') { 14 return platformBrowserDynamic()
15 enableProdMode(); 15 .bootstrapModule(AppModule)
16 .then(decorateModuleRef)
17 .catch(err => console.error(err));
16} 18}
17 19
18bootstrap(AppComponent, [ 20bootloader(main);
19 HTTP_PROVIDERS,
20 provide(AuthHttp, {
21 useFactory: (backend: XHRBackend, defaultOptions: RequestOptions, authService: AuthService) => {
22 return new AuthHttp(backend, defaultOptions, authService);
23 },
24 deps: [ XHRBackend, RequestOptions, AuthService ]
25 }),
26 AuthService,
27 provideRouter(routes)
28]);
diff --git a/client/src/polyfills.ts b/client/src/polyfills.ts
index 740a563bb..65e211459 100644
--- a/client/src/polyfills.ts
+++ b/client/src/polyfills.ts
@@ -6,9 +6,28 @@ require('intl/locale-data/jsonp/en.js');
6import 'ie-shim'; // Internet Explorer 6import 'ie-shim'; // Internet Explorer
7 7
8// Prefer CoreJS over the polyfills above 8// Prefer CoreJS over the polyfills above
9import 'core-js/es6'; 9import 'core-js/es6/symbol';
10import 'core-js/es6/object';
11import 'core-js/es6/function';
12import 'core-js/es6/parse-int';
13import 'core-js/es6/parse-float';
14import 'core-js/es6/number';
15import 'core-js/es6/math';
16import 'core-js/es6/string';
17import 'core-js/es6/date';
18import 'core-js/es6/array';
19import 'core-js/es6/regexp';
20import 'core-js/es6/map';
21import 'core-js/es6/set';
22import 'core-js/es6/weak-map';
23import 'core-js/es6/weak-set';
24import 'core-js/es6/typed';
25import 'core-js/es6/reflect';
26// see issue https://github.com/AngularClass/angular2-webpack-starter/issues/709
27// import 'core-js/es6/promise';
28
10import 'core-js/es7/reflect'; 29import 'core-js/es7/reflect';
11require('zone.js/dist/zone'); 30import 'zone.js/dist/zone';
12 31
13// Typescript emit helpers polyfill 32// Typescript emit helpers polyfill
14import 'ts-helpers'; 33import 'ts-helpers';
diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss
index 9c48b4627..b3bdffe50 100644
--- a/client/src/sass/application.scss
+++ b/client/src/sass/application.scss
@@ -6,6 +6,45 @@ body {
6 } 6 }
7} 7}
8 8
9menu {
10 @media screen and (max-width: 600px) {
11 margin-right: 3px !important;
12 padding: 3px !important;
13 min-height: 400px !important;
14 }
15
16 min-height: 600px;
17 margin-right: 20px;
18 border-right: 1px solid rgba(0, 0, 0, 0.2);
19
20 .panel-block:not(:last-child) {
21 border-bottom: 1px solid rgba(0, 0, 0, 0.1);
22 }
23
24 .panel-button {
25 margin: 8px;
26 cursor: pointer;
27 transition: margin 0.2s;
28
29 &:hover {
30 margin-left: 15px;
31 }
32
33 a {
34 color: #333333;
35 }
36 }
37
38 .glyphicon {
39 margin: 5px;
40 }
41}
42
43.table-column-id {
44 width: 200px;
45}
46
47
9footer { 48footer {
10 border-top: 1px solid rgba(0, 0, 0, 0.2); 49 border-top: 1px solid rgba(0, 0, 0, 0.2);
11 padding-top: 10px; 50 padding-top: 10px;
diff --git a/client/src/vendor.ts b/client/src/vendor.ts
index 8f029191a..95356d9d0 100644
--- a/client/src/vendor.ts
+++ b/client/src/vendor.ts
@@ -8,13 +8,17 @@ import '@angular/platform-browser';
8import '@angular/platform-browser-dynamic'; 8import '@angular/platform-browser-dynamic';
9import '@angular/core'; 9import '@angular/core';
10import '@angular/common'; 10import '@angular/common';
11import '@angular/forms';
11import '@angular/http'; 12import '@angular/http';
12import '@angular/router'; 13import '@angular/router';
13 14
15import '@angularclass/hmr';
16
14// RxJS 17// RxJS
15import 'rxjs/Observable'; 18import 'rxjs/Observable';
16import 'rxjs/Subject'; 19import 'rxjs/Subject';
17import 'rxjs/add/operator/catch'; 20import 'rxjs/add/operator/catch';
21import 'rxjs/add/operator/mergeMap';
18import 'rxjs/add/operator/map'; 22import 'rxjs/add/operator/map';
19import 'rxjs/add/observable/throw'; 23import 'rxjs/add/observable/throw';
20 24