Adding account editing popup and displaying profile picture in the top right corner when logged in

This commit is contained in:
Zoe Roux 2020-03-16 01:29:56 +01:00
parent c8e145d2e5
commit 8154b93f9b
18 changed files with 2683 additions and 1719 deletions

View File

@ -3,7 +3,7 @@
"version": 1, "version": 1,
"newProjectRoot": "projects", "newProjectRoot": "projects",
"projects": { "projects": {
"Kyoo": { "kyoo": {
"projectType": "application", "projectType": "application",
"schematics": { "schematics": {
"@schematics/angular:component": { "@schematics/angular:component": {
@ -71,18 +71,18 @@
"serve": { "serve": {
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular-devkit/build-angular:dev-server",
"options": { "options": {
"browserTarget": "Kyoo:build" "browserTarget": "kyoo:build"
}, },
"configurations": { "configurations": {
"production": { "production": {
"browserTarget": "Kyoo:build:production" "browserTarget": "kyoo:build:production"
} }
} }
}, },
"extract-i18n": { "extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n", "builder": "@angular-devkit/build-angular:extract-i18n",
"options": { "options": {
"browserTarget": "Kyoo:build" "browserTarget": "kyoo:build"
} }
}, },
"lint": { "lint": {
@ -100,5 +100,5 @@
} }
} }
}}, }},
"defaultProject": "Kyoo" "defaultProject": "kyoo"
} }

4159
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,41 +9,41 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^8.2.14", "@angular/animations": "^9.0.6",
"@angular/cdk": "^8.2.3", "@angular/cdk": "^9.1.2",
"@angular/common": "^8.2.14", "@angular/common": "^9.0.6",
"@angular/compiler": "^8.2.14", "@angular/compiler": "^9.0.6",
"@angular/core": "^8.2.14", "@angular/core": "^9.0.6",
"@angular/forms": "^8.2.14", "@angular/forms": "^9.0.6",
"@angular/material": "^8.2.3", "@angular/material": "^9.1.2",
"@angular/platform-browser": "^8.2.14", "@angular/platform-browser": "^9.0.6",
"@angular/platform-browser-dynamic": "^8.2.14", "@angular/platform-browser-dynamic": "^9.0.6",
"@angular/router": "^8.2.14", "@angular/router": "^9.0.6",
"angular-auth-oidc-client": "^10.0.15", "angular-auth-oidc-client": "^10.0.15",
"bootstrap": "^4.4.1", "bootstrap": "^4.4.1",
"detect-browser": "^4.8.0", "detect-browser": "^5.0.0",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"hls.js": "^0.12.4", "hls.js": "^0.13.2",
"jquery": "^3.4.1", "jquery": "^3.4.1",
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"zone.js": "~0.9.1" "zone.js": "~0.10.2"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^0.803.25", "@angular-devkit/build-angular": "^0.900.6",
"@angular/cli": "^8.3.25", "@angular/cli": "^9.0.6",
"@angular/compiler-cli": "^8.2.14", "@angular/compiler-cli": "^9.0.6",
"@angular/language-service": "^8.2.14", "@angular/language-service": "^9.0.6",
"@types/bootstrap": "^4.3.1", "@types/bootstrap": "^4.3.1",
"@types/hls.js": "^0.12.5", "@types/hls.js": "^0.12.5",
"@types/jasmine": "~3.3.8", "@types/jasmine": "~3.5.9",
"@types/jasminewd2": "^2.0.8", "@types/jasminewd2": "^2.0.8",
"@types/jquery": "^3.3.32", "@types/jquery": "^3.3.33",
"@types/node": "~8.9.4", "@types/node": "~13.9.1",
"@types/video.js": "^7.3.3", "@types/video.js": "^7.3.3",
"codelyzer": "^5.2.1", "codelyzer": "^5.2.1",
"protractor": "^5.4.3", "protractor": "^5.4.3",
"ts-node": "~7.0.0", "ts-node": "~8.6.2",
"tslint": "~5.15.0", "tslint": "^5.0.0",
"typescript": "~3.5.3" "typescript": "~3.7.5"
} }
} }

View File

@ -1,16 +1,27 @@
<h1 mat-dialog-title>Account</h1> <h1 mat-dialog-title>Account</h1>
<div mat-dialog-content> <div class="row">
<mat-form-field> <div class="col-8">
<mat-label>Email</mat-label> <mat-form-field class="w-75">
<input matInput name="accountEmail" #accountEmail="ngModel" [(ngModel)]="account.email" required email> <mat-label>Email</mat-label>
</mat-form-field> <input matInput name="accountEmail" [(ngModel)]="account.email" required email>
<br/> </mat-form-field>
<mat-form-field> <br/>
<mat-label>Username</mat-label> <mat-form-field class="w-75">
<input matInput name="accountUsername" #accountUsername="ngModel" [(ngModel)]="account.username"> <mat-label>Username</mat-label>
</mat-form-field> <input matInput name="accountUsername" [(ngModel)]="account.username">
</mat-form-field>
</div>
<div class="col-4">
<input type="file" class="d-none" (change)="onPictureSelected($event)" #fileInput/>
<div class="w-100 profilePicture">
<img [src]="account.picture" alt="Profile picture" fallback="account.svg" #accountImg/>
</div>
<button mat-icon-button class="upload_picture" matTooltipPosition="above" matTooltip="Upload picture" (click)="fileInput.click()">
<mat-icon>photo_camera</mat-icon>
</button>
</div>
</div> </div>
<div mat-dialog-actions fxFlexAlign="end" align="end" style="text-align: end"> <div mat-dialog-actions fxFlexAlign="end" align="end" style="text-align: end">
<button mat-button (click)="cancel()">Cancel</button> <button mat-button (click)="cancel()">Cancel</button>
<button mat-button [mat-dialog-close]="this.account" cdkFocusInitial>Ok</button> <button mat-button (click)="finish()" cdkFocusInitial>Ok</button>
</div> </div>

View File

@ -0,0 +1,38 @@
*
{
box-sizing: border-box;
outline: none !important;
}
*:before, *:after
{
box-sizing: border-box;
}
.upload_picture
{
position: absolute;
bottom: 2%;
left: 0;
right: 0;
margin: auto;
}
.profilePicture
{
padding-top: 100%;
height: 0;
position: relative;
> img
{
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: 100%;
border-radius: 50%;
}
}

View File

@ -1,6 +1,7 @@
import {Component, Inject} from '@angular/core'; import {Component, ElementRef, Inject, ViewChild} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {Account} from "../../models/account"; import {Account} from "../../models/account";
import {HttpClient} from "@angular/common/http";
@Component({ @Component({
@ -10,10 +11,37 @@ import {Account} from "../../models/account";
}) })
export class AccountComponent export class AccountComponent
{ {
constructor(public dialogRef: MatDialogRef<AccountComponent>, @Inject(MAT_DIALOG_DATA) public account: Account) {} selectedPicture: File;
@ViewChild("accountImg") accountImg: ElementRef;
constructor(public dialogRef: MatDialogRef<AccountComponent>, @Inject(MAT_DIALOG_DATA) public account: Account, private http: HttpClient) {}
finish()
{
let data = new FormData();
data.append("email", this.account.email);
data.append("username", this.account.username);
data.append("picture", this.selectedPicture);
this.http.post("api/account/update", data).subscribe(() =>
{
this.dialogRef.close(this.account);
});
}
cancel() cancel()
{ {
this.dialogRef.close(); this.dialogRef.close();
} }
onPictureSelected(event: any)
{
this.selectedPicture = event.target.files[0];
const reader = new FileReader();
reader.onloadend = () =>
{
this.accountImg.nativeElement.src = reader.result;
};
reader.readAsDataURL(this.selectedPicture);
}
} }

View File

@ -13,8 +13,6 @@ import { ShowResolverService } from './services/show-resolver.service';
import { StreamResolverService } from "./services/stream-resolver.service"; import { StreamResolverService } from "./services/stream-resolver.service";
import { ShowDetailsComponent } from './show-details/show-details.component'; import { ShowDetailsComponent } from './show-details/show-details.component';
import {LoginComponent} from "./login/login.component"; import {LoginComponent} from "./login/login.component";
import {AccountComponent} from "./account/account.component";
import {AuthenticatedGuard} from "./guards/authenticated-guard.service";
import {UnauthorizedComponent} from "./unauthorized/unauthorized.component"; import {UnauthorizedComponent} from "./unauthorized/unauthorized.component";
import {LogoutComponent} from "./logout/logout.component"; import {LogoutComponent} from "./logout/logout.component";

View File

@ -25,14 +25,12 @@
</a> </a>
</li> </li>
<ng-template #accountDrop> <ng-template #accountDrop>
<li class="nav-item"> <li class="nav-item icon" style="opacity: 1 !important;">
<button mat-icon-button [matMenuTriggerFor]="accountMenu" class="icon" matTooltipPosition="below" [matTooltip]="authManager.user.name"> <img matRipple [src]="authManager.user.picture" [matMenuTriggerFor]="accountMenu" class="profilePicture" matTooltipPosition="below" [matTooltip]="authManager.user.name" />
<mat-icon>more_vert</mat-icon>
</button>
</li> </li>
<mat-menu #accountMenu="matMenu"> <mat-menu #accountMenu="matMenu">
<button mat-menu-item (click)="this.openAccountDialog()">Settings</button> <button class="dropButton" mat-menu-item (click)="this.openAccountDialog()">Settings</button>
<button mat-menu-item (click)="this.authManager.logout()">Logout</button> <button class="dropButton" mat-menu-item (click)="this.authManager.logout()">Logout</button>
</mat-menu> </mat-menu>
</ng-template> </ng-template>
</ul> </ul>

View File

@ -93,4 +93,18 @@ input::-webkit-search-cancel-button
cursor: pointer; cursor: pointer;
opacity: 1; opacity: 1;
} }
}
.profilePicture
{
width: 24px;
height: 24px;
display: inline-block;
vertical-align: middle;
border-radius: 50%;
}
.dropButton
{
outline: none;
} }

View File

@ -19,7 +19,7 @@ export class AppComponent
isLoading: boolean = false; isLoading: boolean = false;
account: Account; account: Account;
constructor(http: HttpClient, private router: Router, private location: Location, private authManager: AuthService, public dialog: MatDialog) constructor(private http: HttpClient, private router: Router, private location: Location, private authManager: AuthService, public dialog: MatDialog)
{ {
http.get<Library[]>("api/libraries").subscribe(result => http.get<Library[]>("api/libraries").subscribe(result =>
{ {
@ -81,7 +81,11 @@ export class AppComponent
openAccountDialog() openAccountDialog()
{ {
this.dialog.open(AccountComponent, {width: "500px", data: this.account}); const dialog = this.dialog.open(AccountComponent, {width: "500px", data: this.account});
dialog.afterClosed().subscribe((result: Account) =>
{
this.account = result;
});
} }
} }

View File

@ -38,10 +38,11 @@ import {
OpenIdConfiguration OpenIdConfiguration
} from "angular-auth-oidc-client"; } from "angular-auth-oidc-client";
import { AccountComponent } from './account/account.component'; import { AccountComponent } from './account/account.component';
import {AuthenticatedGuard} from "./guards/authenticated-guard.service";
import { UnauthorizedComponent } from './unauthorized/unauthorized.component'; import { UnauthorizedComponent } from './unauthorized/unauthorized.component';
import { LogoutComponent } from './logout/logout.component'; import { LogoutComponent } from './logout/logout.component';
import {MatDialogModule} from '@angular/material/dialog'; import {MatDialogModule} from '@angular/material/dialog';
import {FallbackDirective} from "./misc/fallback.directive";
import {AuthenticatedGuard} from "./misc/guards/authenticated-guard.service";
export function loadConfig(oidcConfigService: OidcConfigService) export function loadConfig(oidcConfigService: OidcConfigService)
{ {
@ -64,7 +65,8 @@ export function loadConfig(oidcConfigService: OidcConfigService)
PasswordValidator, PasswordValidator,
AccountComponent, AccountComponent,
UnauthorizedComponent, UnauthorizedComponent,
LogoutComponent LogoutComponent,
FallbackDirective
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

View File

@ -0,0 +1,18 @@
import {Directive, ElementRef, HostListener, Input} from '@angular/core';
@Directive({
selector: 'img[fallback]'
})
export class FallbackDirective
{
@Input() fallback: string;
constructor(private img: ElementRef) { }
@HostListener("error")
onError()
{
const html: HTMLImageElement = this.img.nativeElement;
html.src = this.fallback;
}
}

View File

@ -10,7 +10,7 @@ import {
Router Router
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import {AuthService} from "../services/auth.service"; import {AuthService} from "../../services/auth.service";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'

View File

@ -57,6 +57,6 @@ export class AuthService
if (!this.isAuthenticated) if (!this.isAuthenticated)
return null; return null;
console.log(this.user); console.log(this.user);
return {email: this.user.email, username: this.user.username}; return {email: this.user.email, username: this.user.username, picture: this.user.picture};
} }
} }

View File

@ -2,4 +2,5 @@ export interface Account
{ {
username: string; username: string;
email: string; email: string;
picture: string;
} }

View File

View File

@ -1,20 +0,0 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: any;
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

1
static/account.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 365 B