commit
0079390753
|
@ -11,6 +11,7 @@ import { Observable } from 'rxjs';
|
|||
const defaultsUrl = '/api/defaults';
|
||||
const deviceUrl = '/api/devices';
|
||||
const geographyUrl = 'api/geographies';
|
||||
const pairingCodeUrl = '/api/pairing-code';
|
||||
const preferencesUrl = '/api/preferences';
|
||||
const voicesUrl = '/api/voices';
|
||||
const wakeWordUrl = '/api/wake-words';
|
||||
|
@ -22,16 +23,24 @@ export class DeviceService {
|
|||
constructor(private http: HttpClient) {
|
||||
}
|
||||
|
||||
getDevices() {
|
||||
getDevices(): Observable<Device[]> {
|
||||
return this.http.get<Device[]>(deviceUrl);
|
||||
}
|
||||
|
||||
getDevice(deviceId: string): Observable<Device> {
|
||||
return this.http.get<Device>(deviceUrl + '/' + deviceId);
|
||||
}
|
||||
|
||||
addDevice(deviceForm: FormGroup) {
|
||||
this.http.post<any>(deviceUrl, deviceForm.value).subscribe();
|
||||
}
|
||||
|
||||
deleteDevice(device: Device): void {
|
||||
console.log('deleting device... ');
|
||||
deleteDevice(device: Device): Observable<any> {
|
||||
return this.http.delete(deviceUrl + '/' + device.id);
|
||||
}
|
||||
|
||||
updateDevice(deviceId: string, deviceForm: FormGroup): Observable<any> {
|
||||
return this.http.patch(deviceUrl + '/' + deviceId, deviceForm.value);
|
||||
}
|
||||
|
||||
addAccountPreferences(preferencesForm: FormGroup) {
|
||||
|
@ -58,6 +67,10 @@ export class DeviceService {
|
|||
return this.http.get<AccountDefaults>(defaultsUrl);
|
||||
}
|
||||
|
||||
validatePairingCode(pairingCode: string): Observable<any> {
|
||||
return this.http.get<Observable<any>>(pairingCodeUrl + '/' + pairingCode);
|
||||
}
|
||||
|
||||
getGeographies() {
|
||||
return this.http.get<DeviceAttribute[]>(geographyUrl);
|
||||
}
|
||||
|
|
|
@ -1,25 +1,18 @@
|
|||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
|
||||
import { Observable, Subject, throwError } from 'rxjs';
|
||||
import { catchError, tap } from 'rxjs/operators';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
import { Account } from '@account/models/account.model';
|
||||
import { AccountMembership } from '@account/models/account-membership.model';
|
||||
import { Agreement } from '@account/models/agreement.model';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { MembershipType } from '@account/models/membership.model';
|
||||
|
||||
|
||||
// URLs for the http requests
|
||||
const ACCOUNT_URL = '/api/account';
|
||||
const AGREEMENT_URL = '/api/agreement/';
|
||||
const MEMBERSHIP_URL = '/api/memberships';
|
||||
|
||||
const fiveSeconds = 5000;
|
||||
|
||||
|
||||
export function storeRedirect() {
|
||||
localStorage.setItem(
|
||||
|
@ -42,32 +35,8 @@ export function navigateToLogin(delay: number): void {
|
|||
|
||||
@Injectable()
|
||||
export class ProfileService {
|
||||
public selectedMembershipType = new Subject<string>();
|
||||
|
||||
constructor(private http: HttpClient, private snackBar: MatSnackBar) {
|
||||
}
|
||||
|
||||
handle400Error(error: HttpErrorResponse) {
|
||||
if (error.status === 400) {
|
||||
this.snackBar.open(
|
||||
'Account creation failed.',
|
||||
null,
|
||||
{panelClass: 'mycroft-snackbar', duration: fiveSeconds}
|
||||
);
|
||||
}
|
||||
if (error.error instanceof ErrorEvent) {
|
||||
// A client-side or network error occurred. Handle it accordingly.
|
||||
console.error('An error occurred:', error.error.message);
|
||||
} else {
|
||||
// The backend returned an unsuccessful response code.
|
||||
// The response body may contain clues as to what went wrong,
|
||||
console.error(
|
||||
`Backend returned code ${error.status}, ` +
|
||||
`body was: ${error.error}`);
|
||||
}
|
||||
// return an observable with a user-facing error message
|
||||
return throwError(
|
||||
'Something bad happened; please try again later.');
|
||||
constructor(private http: HttpClient) {
|
||||
}
|
||||
|
||||
handleError(error: HttpErrorResponse) {
|
||||
|
@ -90,12 +59,6 @@ export class ProfileService {
|
|||
'Something bad happened; please try again later.');
|
||||
}
|
||||
|
||||
addAccount(newAcctForm: FormGroup) {
|
||||
return this.http.post<any>(ACCOUNT_URL, newAcctForm.value).pipe(
|
||||
catchError(this.handle400Error)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* API call to retrieve account info to display.
|
||||
*/
|
||||
|
@ -105,16 +68,6 @@ export class ProfileService {
|
|||
);
|
||||
}
|
||||
|
||||
getAgreement(agreementType: string) {
|
||||
let url_suffix: string;
|
||||
if (agreementType === 'Terms of Use') {
|
||||
url_suffix = 'terms-of-use';
|
||||
} else {
|
||||
url_suffix = 'privacy-policy';
|
||||
}
|
||||
return this.http.get<Agreement>(AGREEMENT_URL + url_suffix);
|
||||
}
|
||||
|
||||
getMembershipTypes(): Observable<MembershipType[]> {
|
||||
return this.http.get<MembershipType[]>(MEMBERSHIP_URL);
|
||||
}
|
||||
|
@ -128,17 +81,4 @@ export class ProfileService {
|
|||
deleteAccount() {
|
||||
return this.http.delete(ACCOUNT_URL);
|
||||
}
|
||||
|
||||
setSelectedMembershipType(accountMembership: AccountMembership, membershipTypes: MembershipType[]) {
|
||||
let selectedMembership: MembershipType;
|
||||
if (accountMembership) {
|
||||
selectedMembership = membershipTypes.find(
|
||||
(membershipType) => membershipType.type === accountMembership.type
|
||||
);
|
||||
this.selectedMembershipType.next(selectedMembership.type);
|
||||
} else {
|
||||
this.selectedMembershipType.next('Maybe Later');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { SkillSettings } from '@account/models/skill-settings.model';
|
|||
|
||||
const accountSkillUrl = '/api/skills';
|
||||
const accountDeviceCountUrl = '/api/device-count';
|
||||
const skillOauthUrl = 'api/skills/oauth';
|
||||
|
||||
|
||||
@Injectable({
|
||||
|
@ -29,12 +30,13 @@ export class SkillService {
|
|||
}
|
||||
|
||||
updateSkillSettings(skillId: string, skillSettings: SkillSettings[]) {
|
||||
this.http.put(
|
||||
return this.http.put(
|
||||
`/api/skills/${skillId}/settings`,
|
||||
{skillSettings: skillSettings}
|
||||
).subscribe(
|
||||
(response) => { console.log(response); }
|
||||
);
|
||||
}
|
||||
|
||||
authenticateSkill(oauthId: number) {
|
||||
return this.http.get(skillOauthUrl + '/' + oauthId.toString(), );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import "~@angular/material/theming";
|
||||
@import "../../../../../../../../../node_modules/@angular/material/theming";
|
||||
@import "mycroft-colors";
|
||||
|
||||
mat-card {
|
|
@ -0,0 +1,18 @@
|
|||
<mat-card [ngClass]="{'mat-elevation-z0': addingDevice}" [formGroup]="defaultsForm">
|
||||
<mat-toolbar>
|
||||
<span *ngIf="addingDevice">Setup Device Defaults</span>
|
||||
<span *ngIf="!addingDevice">Manage Device Defaults</span>
|
||||
</mat-toolbar>
|
||||
<mat-card-content>
|
||||
<account-geography-card
|
||||
[geoForm]="defaultsForm"
|
||||
[required]="true"
|
||||
>
|
||||
</account-geography-card>
|
||||
<account-voice-card [voiceForm]="defaultsForm"></account-voice-card>
|
||||
<account-wake-word-card [wakeWordForm]="defaultsForm">x</account-wake-word-card>
|
||||
</mat-card-content>
|
||||
<mat-card-actions *ngIf="!addingDevice" align="right">
|
||||
<button mat-button (click)="onSave()" [disabled]="!defaultsForm.valid">SAVE</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
|
@ -0,0 +1,14 @@
|
|||
@import "~@angular/material/theming";
|
||||
@import "mycroft-colors";
|
||||
@import "components/buttons";
|
||||
@import "components/cards";
|
||||
|
||||
mat-card {
|
||||
@include section-card;
|
||||
margin-top: 32px;
|
||||
max-width: 700px;
|
||||
|
||||
mat-card-title {
|
||||
color: mat-color($mycroft-primary)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { AccountDefaults } from '@account/models/defaults.model';
|
||||
import { DeviceService } from '@account/http/device.service';
|
||||
import { MatSnackBar, MatSnackBarConfig } from '@angular/material';
|
||||
|
||||
const fiveSeconds = 5000;
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'account-defaults-card',
|
||||
templateUrl: './defaults-card.component.html',
|
||||
styleUrls: ['./defaults-card.component.scss']
|
||||
})
|
||||
export class DefaultsCardComponent implements OnInit {
|
||||
@Input() addingDevice = false;
|
||||
@Input() defaults: AccountDefaults;
|
||||
@Input() defaultsForm: FormGroup;
|
||||
private snackbarConfig = new MatSnackBarConfig();
|
||||
|
||||
constructor(
|
||||
private deviceService: DeviceService,
|
||||
private snackbar: MatSnackBar
|
||||
) {
|
||||
this.snackbarConfig.panelClass = 'mycroft-no-action-snackbar';
|
||||
this.snackbarConfig.duration = fiveSeconds;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
onSave() {
|
||||
if (this.defaults) {
|
||||
this.deviceService.updateAccountDefaults(this.defaultsForm).subscribe(
|
||||
() => {
|
||||
this.defaults = this.defaultsForm.value;
|
||||
this.snackbar.open(
|
||||
'Default values saved',
|
||||
null,
|
||||
this.snackbarConfig
|
||||
);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.deviceService.addAccountDefaults(this.defaultsForm).subscribe(
|
||||
() => {
|
||||
this.defaults = this.defaultsForm.value;
|
||||
this.snackbar.open(
|
||||
'Default values saved',
|
||||
null,
|
||||
this.snackbarConfig
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,62 +2,53 @@
|
|||
<mat-tab label="Configuration">
|
||||
<mat-card-content fxLayout="row wrap">
|
||||
<account-display-field
|
||||
fxFlex="50"
|
||||
[label]="'Voice'"
|
||||
[value]="device.voice.name"
|
||||
[value]="device.voice.displayName"
|
||||
>
|
||||
</account-display-field>
|
||||
<account-display-field
|
||||
fxFlex="50"
|
||||
[label]="'Wake Word'"
|
||||
[value]="device.wakeWord.name"
|
||||
[value]="device.wakeWord.displayName"
|
||||
>
|
||||
</account-display-field>
|
||||
<account-display-field
|
||||
fxFlex="33"
|
||||
[label]="'Platform'"
|
||||
[value]="device.platform"
|
||||
[value]="getPlatform(device)"
|
||||
>
|
||||
</account-display-field>
|
||||
<account-display-field
|
||||
fxFlex="33"
|
||||
[label]="'Core Version'"
|
||||
[value]="device.coreVersion"
|
||||
>
|
||||
</account-display-field>
|
||||
<account-display-field
|
||||
fxFlex="33"
|
||||
[label]="'Enclosure Version'"
|
||||
[value]="device.enclosureVersion"
|
||||
>
|
||||
</account-display-field>
|
||||
<account-display-field
|
||||
[label]="'Core Version'"
|
||||
[value]="device.coreVersion"
|
||||
>
|
||||
</account-display-field>
|
||||
</mat-card-content>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab label="Location">
|
||||
<mat-card-content fxLayout="row wrap">
|
||||
<account-display-field
|
||||
fxFlex="50"
|
||||
[label]="'Country'"
|
||||
[value]="device.geography.country"
|
||||
[value]="device.country.name"
|
||||
>
|
||||
</account-display-field>
|
||||
<account-display-field
|
||||
fxFlex="50"
|
||||
[label]="'Time Zone'"
|
||||
[value]="device.geography.timezone"
|
||||
[value]="device.timezone.name"
|
||||
>
|
||||
</account-display-field>
|
||||
<account-display-field
|
||||
fxFlex="50"
|
||||
[label]="'Region'"
|
||||
[value]="device.geography.region"
|
||||
[value]="device.region.name"
|
||||
>
|
||||
</account-display-field>
|
||||
<account-display-field
|
||||
fxFlex="50"
|
||||
[label]="'City'"
|
||||
[value]="device.geography.city"
|
||||
[value]="device.city.name"
|
||||
>
|
||||
</account-display-field>
|
||||
<mat-form-field>
|
|
@ -0,0 +1,28 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Device } from '@account/models/device.model';
|
||||
|
||||
@Component({
|
||||
selector: 'account-device-info',
|
||||
templateUrl: './device-display.component.html',
|
||||
styleUrls: ['./device-display.component.scss']
|
||||
})
|
||||
export class DeviceDisplayComponent implements OnInit {
|
||||
@Input() device: Device;
|
||||
public platforms = {
|
||||
'mycroft_mark_1': {icon: '../assets/mark-1-icon.svg', displayName: 'Mark I'},
|
||||
'mycroft_mark_2': {icon: '../assets/mark-2-icon.svg', displayName: 'Mark II'},
|
||||
'picroft': {icon: '../assets/picroft-icon.svg', displayName: 'Picroft'},
|
||||
'kde': {icon: '../assets/kde-icon.svg', displayName: 'KDE'}
|
||||
};
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
getPlatform(device: Device) {
|
||||
const knownPlatform = this.platforms[device.platform];
|
||||
return knownPlatform ? knownPlatform.displayName : device.platform;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
<mat-card [ngClass]="{'mat-elevation-z0': addDevice}" [formGroup]="deviceForm">
|
||||
|
||||
<mat-card-title *ngIf="addDevice">Configure your new device</mat-card-title>
|
||||
<mat-card-title *ngIf="!addDevice">Manage your device configuration</mat-card-title>
|
||||
|
||||
<mat-card-content fxLayout="column">
|
||||
|
||||
<mat-card id="id-card" class="mat-elevation-z0" fxLayout="column">
|
||||
<mat-form-field *ngIf="addDevice" [appearance]="'outline'">
|
||||
<mat-label>Pairing Code</mat-label>
|
||||
<input
|
||||
matInput
|
||||
required
|
||||
type="text"
|
||||
onInput="this.value = this.value.toUpperCase()"
|
||||
formControlName="pairingCode"
|
||||
>
|
||||
<mat-error *ngIf="deviceForm.controls['pairingCode'].invalid">
|
||||
{{getPairingCodeError()}}
|
||||
</mat-error>
|
||||
<mat-hint>Code spoken by device</mat-hint>
|
||||
</mat-form-field>
|
||||
<div fxLayout="row wrap">
|
||||
<mat-form-field [appearance]="'outline'">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput required type="text" formControlName="name">
|
||||
<mat-hint>Must be unique</mat-hint>
|
||||
</mat-form-field>
|
||||
<mat-form-field [appearance]="'outline'">
|
||||
<mat-label>Placement</mat-label>
|
||||
<input matInput type="text" formControlName="placement">
|
||||
<mat-hint>e.g. Kitchen, Bedroom, Office</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</mat-card>
|
||||
|
||||
<account-geography-card [geoForm]="deviceForm" [required]="true"></account-geography-card>
|
||||
<account-voice-card [voiceForm]="deviceForm"></account-voice-card>
|
||||
<account-wake-word-card [wakeWordForm]="deviceForm">
|
||||
</account-wake-word-card>
|
||||
</mat-card-content>
|
||||
|
||||
<mat-card-actions *ngIf=!addDevice align="right">
|
||||
<button mat-button id="cancel-button" (click)="onCancel()">CANCEL</button>
|
||||
<button mat-button [disabled]="!deviceForm.valid" (click)="onSave()">SAVE</button>
|
||||
</mat-card-actions>
|
||||
|
||||
</mat-card>
|
|
@ -3,19 +3,25 @@
|
|||
@import "components/buttons";
|
||||
@import "components/cards";
|
||||
|
||||
|
||||
#id-card {
|
||||
margin: 0;
|
||||
max-width: 700px;
|
||||
padding: 16px;
|
||||
|
||||
mat-form-field {
|
||||
margin-left: 16px;
|
||||
margin-top: 16px;
|
||||
min-width: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
mat-card {
|
||||
@include section-card;
|
||||
max-width: 700px;
|
||||
margin-bottom: 16px;
|
||||
margin-top: 32px;
|
||||
|
||||
mat-card-title {
|
||||
color: mat-color($mycroft-primary)
|
||||
}
|
||||
|
||||
mat-card-content {
|
||||
margin: 16px;
|
||||
|
||||
.mat-h2 {
|
||||
color: mat-color($mycroft-accent, A700);
|
||||
margin-top: 32px;
|
||||
|
@ -31,10 +37,11 @@ mat-card {
|
|||
@include action-button-primary;
|
||||
margin-bottom: 16px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: mat-color($mycroft-accent, 200);
|
||||
}
|
||||
#cancel-button {
|
||||
background-color: white;
|
||||
color: mat-color($mycroft-accent);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'account-device-edit-card',
|
||||
templateUrl: './device-edit-card.component.html',
|
||||
styleUrls: ['./device-edit-card.component.scss']
|
||||
})
|
||||
export class DeviceEditCardComponent implements OnInit {
|
||||
@Input() deviceForm: FormGroup;
|
||||
@Input() addDevice = false;
|
||||
@Output() saveChanges = new EventEmitter<boolean>();
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
onSave() {
|
||||
this.saveChanges.emit(true);
|
||||
}
|
||||
|
||||
onCancel() {
|
||||
this.saveChanges.emit(false);
|
||||
}
|
||||
|
||||
getPairingCodeError(): string {
|
||||
let errorMessage = '';
|
||||
const pairingCodeControl = this.deviceForm.controls['pairingCode'];
|
||||
if (pairingCodeControl.hasError('required')) {
|
||||
errorMessage = 'This value is required';
|
||||
} else if (pairingCodeControl.hasError('minlength')) {
|
||||
errorMessage = 'Pairing code must be six characters';
|
||||
} else if (pairingCodeControl.hasError('maxlength')) {
|
||||
errorMessage = 'Pairing code must be six characters';
|
||||
} else if (pairingCodeControl.hasError('unknownPairingCode')) {
|
||||
errorMessage = 'Unknown pairing code';
|
||||
}
|
||||
return errorMessage;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<mat-card class="mat-elevation-z0">
|
||||
<mat-card-content>
|
||||
<h2 class="mat-h2">Geographical Location</h2>
|
||||
<div fxLayout="row wrap" fxLayoutAlign='start' fxLayout.xs="column">
|
||||
<account-country-input fxFlex.gt-xs="40"
|
||||
[countryControl]="countryControl"
|
||||
[required]="false"
|
||||
(countrySelected)="onCountrySelect($event)"
|
||||
>
|
||||
</account-country-input>
|
||||
<account-region-input fxFlex.gt-xs="40"
|
||||
[country]="selectedCountry"
|
||||
[regionControl]="regionControl"
|
||||
[required]="false"
|
||||
(regionSelected)="onRegionSelect($event)"
|
||||
>
|
||||
</account-region-input>
|
||||
<account-city-input fxFlex.gt-xs="40"
|
||||
[cityControl]="cityControl"
|
||||
(citySelected)="onCitySelect($event)"
|
||||
[region]="selectedRegion"
|
||||
[required]="false"
|
||||
>
|
||||
</account-city-input>
|
||||
<account-timezone-input fxFlex.gt-xs="40"
|
||||
[country]="selectedCountry"
|
||||
[required]="false"
|
||||
[timezoneControl]="timezoneControl"
|
||||
>
|
||||
</account-timezone-input>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
|
@ -0,0 +1,73 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { AbstractControl, FormGroup } from '@angular/forms';
|
||||
import { Country } from '@account/models/country.model';
|
||||
import { Region } from '@account/models/region.model';
|
||||
import { City } from '@account/models/city.model';
|
||||
import { AccountDefaults } from '@account/models/defaults.model';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'account-geography-card',
|
||||
templateUrl: './geography-card.component.html',
|
||||
styleUrls: ['./geography-card.component.scss']
|
||||
})
|
||||
export class GeographyCardComponent implements OnInit {
|
||||
@Input() geographyRequired: boolean;
|
||||
@Input() geoForm: FormGroup;
|
||||
@Input() required: boolean;
|
||||
public countryControl: AbstractControl;
|
||||
public regionControl: AbstractControl;
|
||||
public cityControl: AbstractControl;
|
||||
public timezoneControl: AbstractControl;
|
||||
public selectedCity: City;
|
||||
public selectedCountry = new Subject<Country>();
|
||||
public selectedRegion = new Subject<Region>();
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.countryControl = this.geoForm.controls['country'];
|
||||
this.regionControl = this.geoForm.controls['region'];
|
||||
this.cityControl = this.geoForm.controls['city'];
|
||||
this.timezoneControl = this.geoForm.controls['timezone'];
|
||||
this.cityControl.disable();
|
||||
this.regionControl.disable();
|
||||
this.timezoneControl.disable();
|
||||
}
|
||||
|
||||
onCountrySelect(selectedCountry: Country): void {
|
||||
if (selectedCountry) {
|
||||
this.selectedCountry.next(selectedCountry);
|
||||
this.regionControl.enable();
|
||||
this.timezoneControl.enable();
|
||||
} else {
|
||||
this.cityControl.disable();
|
||||
this.cityControl.setValue('');
|
||||
this.regionControl.disable();
|
||||
this.regionControl.setValue('');
|
||||
this.timezoneControl.disable();
|
||||
this.timezoneControl.setValue('');
|
||||
}
|
||||
}
|
||||
|
||||
onRegionSelect(selectedRegion: Region): void {
|
||||
if (selectedRegion) {
|
||||
this.selectedRegion.next(selectedRegion);
|
||||
this.cityControl.enable();
|
||||
} else {
|
||||
this.cityControl.disable();
|
||||
this.cityControl.setValue('');
|
||||
}
|
||||
}
|
||||
|
||||
onCitySelect(selectedCity: City): void {
|
||||
if (selectedCity) {
|
||||
this.selectedCity = selectedCity;
|
||||
this.timezoneControl.setValue(selectedCity.timezone);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<mat-card [ngClass]="{'mat-elevation-z0': addingDevice}" [formGroup]="preferencesForm" class="mat-elevation-z0">
|
||||
<mat-toolbar>
|
||||
<span *ngIf="addingDevice">Setup Device Preferences</span>
|
||||
<span *ngIf="!addingDevice">Manage Device Preferences</span>
|
||||
</mat-toolbar>
|
||||
<mat-card-content fxLayout="column">
|
||||
<p class="mat-body">
|
||||
Preferences are applied to all your devices to present information
|
||||
in a manner you are accustomed to.
|
||||
</p>
|
||||
<account-option-btn
|
||||
[config]="measurementOptionsConfig"
|
||||
formControlName="measurementSystem"
|
||||
>
|
||||
</account-option-btn>
|
||||
<account-option-btn
|
||||
[config]="timeFormatOptionsConfig"
|
||||
formControlName="timeFormat"
|
||||
>
|
||||
</account-option-btn>
|
||||
<account-option-btn
|
||||
[config]="dateFormatOptionsConfig"
|
||||
formControlName="dateFormat"
|
||||
>
|
||||
</account-option-btn>
|
||||
</mat-card-content>
|
||||
<mat-card-actions align="right">
|
||||
<ng-content select="button"></ng-content>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
|
@ -0,0 +1,13 @@
|
|||
@import "~@angular/material/theming";
|
||||
@import "mycroft-colors";
|
||||
@import "components/buttons";
|
||||
@import "components/cards";
|
||||
|
||||
mat-card {
|
||||
@include section-card;
|
||||
max-width: 700px;
|
||||
|
||||
mat-card-title {
|
||||
color: mat-color($mycroft-primary)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { OptionButtonsConfig } from '@account/models/option-buttons-config.model';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'account-preferences-card',
|
||||
templateUrl: './preferences-card.component.html',
|
||||
styleUrls: ['./preferences-card.component.scss']
|
||||
})
|
||||
export class PreferencesCardComponent implements OnInit {
|
||||
@Input() addingDevice = false;
|
||||
@Input() preferencesForm: FormGroup;
|
||||
public measurementOptionsConfig: OptionButtonsConfig;
|
||||
public timeFormatOptionsConfig: OptionButtonsConfig;
|
||||
public dateFormatOptionsConfig: OptionButtonsConfig;
|
||||
|
||||
constructor() {
|
||||
this.dateFormatOptionsConfig = {
|
||||
label: 'Date Format',
|
||||
options: ['DD/MM/YYYY', 'MM/DD/YYYY'],
|
||||
buttonWidth: '130px',
|
||||
labelWidth: '180px'
|
||||
};
|
||||
this.measurementOptionsConfig = {
|
||||
label: 'Measurement System',
|
||||
options: ['Imperial', 'Metric'],
|
||||
buttonWidth: '130px',
|
||||
labelWidth: '180px'
|
||||
};
|
||||
this.timeFormatOptionsConfig = {
|
||||
label: 'Time Format',
|
||||
options: ['12 Hour', '24 Hour'],
|
||||
buttonWidth: '130px',
|
||||
labelWidth: '180px'
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<mat-card class="mat-elevation-z0" [formGroup]="voiceForm">
|
||||
<mat-card-content>
|
||||
<h2 class="mat-h2">Voice</h2>
|
||||
<account-option-btn [config]="voiceOptionsConfig" formControlName="voice">
|
||||
</account-option-btn>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
|
@ -0,0 +1,24 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { OptionButtonsConfig } from '@account/models/option-buttons-config.model';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'account-voice-card',
|
||||
templateUrl: './voice-card.component.html',
|
||||
styleUrls: ['./voice-card.component.scss']
|
||||
})
|
||||
export class VoiceCardComponent implements OnInit {
|
||||
@Input() voiceForm: FormGroup;
|
||||
public voiceOptionsConfig: OptionButtonsConfig;
|
||||
|
||||
constructor() {
|
||||
this.voiceOptionsConfig = {
|
||||
options: ['British Male', 'American Female', 'American Male', 'Google Voice'],
|
||||
buttonWidth: '140px'
|
||||
};
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<mat-card class="mat-elevation-z0" [formGroup]="wakeWordForm">
|
||||
<mat-card-content>
|
||||
<h2 class="mat-h2">Wake Word</h2>
|
||||
<account-option-btn
|
||||
[config]="wakeWordOptionsConfig"
|
||||
formControlName="wakeWord"
|
||||
>
|
||||
</account-option-btn>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
|
@ -0,0 +1,25 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { OptionButtonsConfig } from '@account/models/option-buttons-config.model';
|
||||
|
||||
@Component({
|
||||
selector: 'account-wake-word-card',
|
||||
templateUrl: './wake-word-card.component.html',
|
||||
styleUrls: ['./wake-word-card.component.scss']
|
||||
})
|
||||
export class WakeWordCardComponent implements OnInit {
|
||||
@Input() wakeWordForm: FormGroup;
|
||||
public wakeWordOptionsConfig: OptionButtonsConfig;
|
||||
|
||||
|
||||
constructor() {
|
||||
this.wakeWordOptionsConfig = {
|
||||
options: ['Hey Mycroft', 'Christopher', 'Hey Ezra', 'Hey Jarvis'],
|
||||
buttonWidth: '130px'
|
||||
};
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
|
@ -1,18 +1,15 @@
|
|||
<ng-container [formGroup]="deviceForm">
|
||||
<mat-form-field fxFlex [appearance]="'outline'">
|
||||
<mat-label>City</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
formControlName="city"
|
||||
[matAutocomplete]="cityComplete"
|
||||
[required]="required"
|
||||
(focus)="getCities()"
|
||||
>
|
||||
<mat-autocomplete #cityComplete="matAutocomplete">
|
||||
<mat-option *ngFor="let city of filteredCities$ | async" [value]="city.name">
|
||||
{{city.name}}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
</ng-container>
|
||||
<mat-form-field fxFlex [appearance]="'outline'">
|
||||
<mat-label>City</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[formControl]="cityControl"
|
||||
[matAutocomplete]="cityComplete"
|
||||
[required]="required"
|
||||
>
|
||||
<mat-autocomplete #cityComplete="matAutocomplete">
|
||||
<mat-option *ngFor="let city of filteredCities$ | async" [value]="city.name">
|
||||
{{city.name}}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
mat-form-field {
|
||||
margin-left: 16px;
|
||||
}
|
|
@ -1,47 +1,51 @@
|
|||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { AbstractControl, ValidatorFn } from '@angular/forms';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { startWith, map, tap} from 'rxjs/operators';
|
||||
|
||||
import { City } from '../../../../../shared/models/city.model';
|
||||
import { AbstractControl, FormGroup, ValidatorFn } from '@angular/forms';
|
||||
import { City } from '@account/models/city.model';
|
||||
import { GeographyService } from '@account/http/geography_service';
|
||||
import { Region } from '@account/models/region.model';
|
||||
|
||||
@Component({
|
||||
selector: 'account-city-input',
|
||||
templateUrl: './city-input.component.html',
|
||||
styleUrls: ['./city-input.component.scss']
|
||||
})
|
||||
export class CityInputComponent implements OnInit {
|
||||
@Input() cities$: Observable<City[]>;
|
||||
private cities: City[];
|
||||
@Input() deviceForm: FormGroup;
|
||||
public filteredCities$ = new Observable<City[]>();
|
||||
@Output() citySelected = new EventEmitter<City>();
|
||||
private cityControl: AbstractControl;
|
||||
export class CityInputComponent implements OnDestroy, OnInit {
|
||||
@Input() cityControl: AbstractControl;
|
||||
@Input() region: Subject<Region>;
|
||||
@Input() required: boolean;
|
||||
@Output() citySelected = new EventEmitter<City>();
|
||||
public cities: City[];
|
||||
public filteredCities$ = new Observable<City[]>();
|
||||
|
||||
constructor() {
|
||||
constructor(private geoService: GeographyService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.cityControl = this.deviceForm.controls['city'];
|
||||
this.cityControl.disable();
|
||||
ngOnInit(): void {
|
||||
this.region.subscribe(
|
||||
(region) => { this.getCities(region); }
|
||||
);
|
||||
}
|
||||
|
||||
getCities() {
|
||||
if (!this.cities) {
|
||||
this.cities$.subscribe(
|
||||
(cities) => {
|
||||
this.cities = cities;
|
||||
this.cityControl.validator = this.cityValidator();
|
||||
this.filteredCities$ = this.cityControl.valueChanges.pipe(
|
||||
startWith(''),
|
||||
map((value) => this.filterCities(value)),
|
||||
tap(() => { this.checkForValidCity(); })
|
||||
ngOnDestroy(): void {
|
||||
this.region.unsubscribe();
|
||||
}
|
||||
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
getCities(region: Region) {
|
||||
this.geoService.getCitiesByRegion(region).subscribe(
|
||||
(cities) => {
|
||||
this.cities = cities;
|
||||
this.cityControl.validator = this.cityValidator();
|
||||
this.filteredCities$ = this.cityControl.valueChanges.pipe(
|
||||
startWith(''),
|
||||
map((value) => this.filterCities(value)),
|
||||
tap(() => { this.emitSelectedCity(); })
|
||||
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private filterCities(value: string): City[] {
|
||||
|
@ -75,7 +79,7 @@ export class CityInputComponent implements OnInit {
|
|||
};
|
||||
}
|
||||
|
||||
checkForValidCity() {
|
||||
emitSelectedCity() {
|
||||
if (this.cityControl.valid) {
|
||||
if (this.cityControl.value) {
|
||||
const foundCity = this.cities.find(
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
<ng-container [formGroup]="deviceForm">
|
||||
<mat-form-field fxFlex [appearance]="'outline'">
|
||||
<mat-label>Country</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="required"
|
||||
type="text"
|
||||
formControlName="country"
|
||||
[matAutocomplete]="countryComplete"
|
||||
(focus)="getCountries()"
|
||||
>
|
||||
<mat-autocomplete #countryComplete="matAutocomplete">
|
||||
<mat-option *ngFor="let country of filteredCountries$ | async" [value]="country.name">
|
||||
{{country.name}}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
</ng-container>
|
||||
<mat-form-field fxFlex [appearance]="'outline'">
|
||||
<mat-label>Country</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="required"
|
||||
type="text"
|
||||
[formControl]="countryControl"
|
||||
[matAutocomplete]="countryComplete"
|
||||
>
|
||||
<mat-autocomplete #countryComplete="matAutocomplete">
|
||||
<mat-option *ngFor="let country of filteredCountries$ | async" [value]="country.name">
|
||||
{{country.name}}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
|
||||
mat-form-field {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { AbstractControl, FormGroup, ValidatorFn } from '@angular/forms';
|
|||
import { map, startWith, tap } from 'rxjs/operators';
|
||||
|
||||
import { Country } from '@account/models/country.model';
|
||||
import { GeographyService } from '@account/http/geography_service';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
|
@ -11,35 +12,43 @@ import { Observable } from 'rxjs';
|
|||
styleUrls: ['./country-input.component.scss']
|
||||
})
|
||||
export class CountryInputComponent implements OnInit {
|
||||
@Input() countries$: Observable<Country[]>;
|
||||
private countries: Country[];
|
||||
private countryControl: AbstractControl;
|
||||
@Input() countryControl: AbstractControl;
|
||||
@Output() countrySelected = new EventEmitter<Country>();
|
||||
@Input() deviceForm: FormGroup;
|
||||
public filteredCountries$: Observable<Country[]>;
|
||||
@Input() required: boolean;
|
||||
|
||||
constructor() { }
|
||||
constructor(private geoService: GeographyService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.countryControl = this.deviceForm.controls['country'];
|
||||
this.geoService.getCountries().subscribe(
|
||||
(countries) => {
|
||||
this.countries = countries;
|
||||
this.countryControl.validator = this.countryValidator();
|
||||
this.filteredCountries$ = this.countryControl.valueChanges.pipe(
|
||||
startWith(''),
|
||||
map((value) => this.filterCountries(value)),
|
||||
tap(() => { this.emitSelectedCountry(); })
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getCountries() {
|
||||
if (!this.countries) {
|
||||
this.countries$.subscribe(
|
||||
(countries) => {
|
||||
this.countries = countries;
|
||||
this.countryControl.validator = this.countryValidator();
|
||||
this.filteredCountries$ = this.countryControl.valueChanges.pipe(
|
||||
startWith(''),
|
||||
map((value) => this.filterCountries(value)),
|
||||
tap(() => { this.checkForValidCountry(); })
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
// getCountries() {
|
||||
// if (!this.countries) {
|
||||
// this.geoService.getCountries().subscribe(
|
||||
// (countries) => {
|
||||
// this.countries = countries;
|
||||
// this.countryControl.validator = this.countryValidator();
|
||||
// this.filteredCountries$ = this.countryControl.valueChanges.pipe(
|
||||
// startWith(''),
|
||||
// map((value) => this.filterCountries(value)),
|
||||
// tap(() => { this.emitSelectedCountry(); })
|
||||
// );
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
private filterCountries(value: string): Country[] {
|
||||
const filterValue = value.toLowerCase();
|
||||
|
@ -72,7 +81,7 @@ export class CountryInputComponent implements OnInit {
|
|||
};
|
||||
}
|
||||
|
||||
checkForValidCountry() {
|
||||
emitSelectedCountry() {
|
||||
if (this.countryControl.valid) {
|
||||
if (this.countryControl.value) {
|
||||
const foundCountry = this.countries.find(
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
<ng-container [formGroup]="deviceForm">
|
||||
<mat-form-field fxFlex [appearance]="'outline'">
|
||||
<mat-label>Region</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
formControlName="region"
|
||||
[matAutocomplete]="regionComplete"
|
||||
[required]="required"
|
||||
(focus)="getRegions()"
|
||||
>
|
||||
<mat-autocomplete #regionComplete="matAutocomplete">
|
||||
<mat-option *ngFor="let region of filteredRegions$ | async" [value]="region.name">
|
||||
{{region.name}}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
</ng-container>
|
||||
<mat-form-field fxFlex [appearance]="'outline'">
|
||||
<mat-label>Region</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[formControl]="regionControl"
|
||||
[matAutocomplete]="regionComplete"
|
||||
[required]="required"
|
||||
>
|
||||
<mat-autocomplete #regionComplete="matAutocomplete">
|
||||
<mat-option *ngFor="let region of filteredRegions$ | async" [value]="region.name">
|
||||
{{region.name}}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
mat-form-field {
|
||||
margin-left: 16px;
|
||||
}
|
|
@ -1,48 +1,52 @@
|
|||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core';
|
||||
import { map, startWith, tap } from 'rxjs/operators';
|
||||
import { Region } from '../../../../../shared/models/region.model';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Region } from '@account/models/region.model';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { AbstractControl, FormGroup, ValidatorFn } from '@angular/forms';
|
||||
import { GeographyService } from '@account/http/geography_service';
|
||||
import { Country } from '@account/models/country.model';
|
||||
|
||||
@Component({
|
||||
selector: 'account-region-input',
|
||||
templateUrl: './region-input.component.html',
|
||||
styleUrls: ['./region-input.component.scss']
|
||||
})
|
||||
export class RegionInputComponent implements OnInit {
|
||||
@Input() deviceForm: FormGroup;
|
||||
@Input() filteredRegions$: Observable<Region[]>;
|
||||
@Input() regions$: Observable<Region[]>;
|
||||
private regions: Region[];
|
||||
private regionControl: AbstractControl;
|
||||
@Output() regionSelected = new EventEmitter<Region>();
|
||||
export class RegionInputComponent implements OnDestroy, OnInit {
|
||||
@Input() country: Subject<Country>;
|
||||
@Input() regionControl: AbstractControl;
|
||||
@Input() required: boolean;
|
||||
@Output() regionSelected = new EventEmitter<Region>();
|
||||
public filteredRegions$: Observable<Region[]>;
|
||||
public regions: Region[];
|
||||
|
||||
constructor() { }
|
||||
constructor(private geoService: GeographyService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.regionControl = this.deviceForm.controls['region'];
|
||||
this.regionControl.disable();
|
||||
ngOnInit(): void {
|
||||
this.country.subscribe(
|
||||
(country) => { this.getRegions(country); }
|
||||
);
|
||||
}
|
||||
|
||||
getRegions() {
|
||||
if (!this.regionControl.value) {
|
||||
this.regions$.subscribe(
|
||||
(regions) => {
|
||||
this.regions = regions;
|
||||
this.regionControl.validator = this.regionValidator();
|
||||
this.filteredRegions$ = this.regionControl.valueChanges.pipe(
|
||||
startWith(''),
|
||||
map((value) => this.filterRegions(value)),
|
||||
tap(() => {this.checkForValidRegion(); })
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
ngOnDestroy(): void {
|
||||
this.country.unsubscribe();
|
||||
}
|
||||
|
||||
getRegions(country: Country) {
|
||||
this.geoService.getRegionsByCountry(country).subscribe(
|
||||
(regions) => {
|
||||
this.regions = regions;
|
||||
this.regionControl.validator = this.regionValidator();
|
||||
this.filteredRegions$ = this.regionControl.valueChanges.pipe(
|
||||
startWith(''),
|
||||
map((value) => this.filterRegions(value)),
|
||||
tap(() => {this.emitSelectedRegion(); })
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private filterRegions(value: string): Region[] {
|
||||
const filterValue = value.toLowerCase();
|
||||
const filterValue = value ? value.toLowerCase() : '';
|
||||
let filteredRegions: Region[];
|
||||
|
||||
if (this.regions) {
|
||||
|
@ -72,7 +76,7 @@ export class RegionInputComponent implements OnInit {
|
|||
};
|
||||
}
|
||||
|
||||
checkForValidRegion() {
|
||||
emitSelectedRegion() {
|
||||
if (this.regionControl.valid) {
|
||||
if (this.regionControl.value) {
|
||||
const foundRegion = this.regions.find(
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
<ng-container [formGroup]="deviceForm">
|
||||
<mat-form-field fxFlex [appearance]="'outline'">
|
||||
<mat-label>Time Zone</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
formControlName="timezone"
|
||||
[matAutocomplete]="timezoneComplete"
|
||||
[required]="required"
|
||||
(focus)="getTimezones()"
|
||||
>
|
||||
<mat-autocomplete #timezoneComplete="matAutocomplete">
|
||||
<mat-option *ngFor="let timezone of filteredTimezones$ | async" [value]="timezone.name">
|
||||
{{timezone.name}}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
</ng-container>
|
||||
<mat-form-field fxFlex [appearance]="'outline'">
|
||||
<mat-label>Time Zone</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[formControl]="timezoneControl"
|
||||
[matAutocomplete]="timezoneComplete"
|
||||
[required]="required"
|
||||
>
|
||||
<mat-autocomplete #timezoneComplete="matAutocomplete">
|
||||
<mat-option *ngFor="let timezone of filteredTimezones$ | async" [value]="timezone.name">
|
||||
{{timezone.name}}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
mat-form-field {
|
||||
margin-left: 16px;
|
||||
}
|
|
@ -1,44 +1,48 @@
|
|||
import { Component, Input, OnInit, Output } from '@angular/core';
|
||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { AbstractControl, FormGroup, ValidatorFn } from '@angular/forms';
|
||||
import { Observable } from 'rxjs';
|
||||
import { startWith, map, tap } from 'rxjs/operators';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { startWith, map } from 'rxjs/operators';
|
||||
|
||||
import { Timezone } from '../../../../../shared/models/timezone.model';
|
||||
import { Timezone } from '@account/models/timezone.model';
|
||||
import { GeographyService } from '@account/http/geography_service';
|
||||
import { Country } from '@account/models/country.model';
|
||||
|
||||
@Component({
|
||||
selector: 'account-timezone-input',
|
||||
templateUrl: './timezone-input.component.html',
|
||||
styleUrls: ['./timezone-input.component.scss']
|
||||
})
|
||||
export class TimezoneInputComponent implements OnInit {
|
||||
@Input() deviceForm: FormGroup;
|
||||
public filteredTimezones$ = new Observable<Timezone[]>();
|
||||
export class TimezoneInputComponent implements OnDestroy, OnInit {
|
||||
@Input() country: Subject<Country>;
|
||||
@Input() required: boolean;
|
||||
@Input() timezones$: Observable<Timezone[]>;
|
||||
@Input() timezoneControl: AbstractControl;
|
||||
public filteredTimezones$ = new Observable<Timezone[]>();
|
||||
private timezones: Timezone[];
|
||||
private timezoneControl: AbstractControl;
|
||||
|
||||
constructor() { }
|
||||
constructor(private geoService: GeographyService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.timezoneControl = this.deviceForm.controls['timezone'];
|
||||
this.timezoneControl.disable();
|
||||
this.country.subscribe(
|
||||
(country) => { this.getTimezones(country); }
|
||||
);
|
||||
}
|
||||
|
||||
getTimezones() {
|
||||
if (!this.timezoneControl.value) {
|
||||
this.timezones$.subscribe(
|
||||
(timezones) => {
|
||||
this.timezones = timezones;
|
||||
this.timezoneControl.validator = this.timezoneValidator();
|
||||
this.filteredTimezones$ = this.timezoneControl.valueChanges.pipe(
|
||||
startWith(''),
|
||||
map((value) => this.filterTimezones(value)),
|
||||
ngOnDestroy(): void {
|
||||
this.country.unsubscribe();
|
||||
}
|
||||
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
getTimezones(country: Country): void {
|
||||
this.geoService.getTimezonesByCountry(country).subscribe(
|
||||
(timezones) => {
|
||||
this.timezones = timezones;
|
||||
this.timezoneControl.validator = this.timezoneValidator();
|
||||
this.filteredTimezones$ = this.timezoneControl.valueChanges.pipe(
|
||||
startWith(''),
|
||||
map((value) => this.filterTimezones(value)),
|
||||
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private filterTimezones(value: string): Timezone[] {
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<mat-card class="mat-elevation-z0">
|
||||
<mat-card-title mat-dialog-title>Remove Device?</mat-card-title>
|
||||
|
||||
<mat-card-content mat-dialog-content>
|
||||
<p class="mat-body">
|
||||
Just double checking. Device removal cannot be undone.
|
||||
</p>
|
||||
</mat-card-content>
|
||||
|
||||
<mat-card-actions mat-dialog-actions [align]="'end'">
|
||||
<button id="device-remove-cancel-button" mat-button (click)="onCancelClick()">CANCEL</button>
|
||||
<button id="device-remove-button" mat-button [mat-dialog-close]="true">REMOVE</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
|
@ -1,5 +1,12 @@
|
|||
@import '../../../../../../../src/stylesheets/components/buttons';
|
||||
@import 'components/buttons';
|
||||
|
||||
mat-card {
|
||||
padding: 0;
|
||||
|
||||
mat-card-actions {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
.mat-body{
|
||||
margin-bottom: 16px;
|
||||
width: 250px;
|
|
@ -3,13 +3,13 @@ import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
|
|||
|
||||
@Component({
|
||||
selector: 'account-device-remove',
|
||||
templateUrl: './remove.component.html',
|
||||
styleUrls: ['./remove.component.scss']
|
||||
templateUrl: './remove-device-dialog.component.html',
|
||||
styleUrls: ['./remove-device-dialog.component.scss']
|
||||
})
|
||||
export class RemoveComponent implements OnInit {
|
||||
export class RemoveDeviceDialogComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<RemoveComponent>,
|
||||
public dialogRef: MatDialogRef<RemoveDeviceDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: boolean) {
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
<mat-card class="mat-elevation-z0" id="default-settings-card" [formGroup]="defaultsForm">
|
||||
<mat-card-title *ngIf="deviceSetup">Setup your device defaults</mat-card-title>
|
||||
<mat-card-title *ngIf="!deviceSetup">Manage your device defaults</mat-card-title>
|
||||
|
||||
<mat-card-content>
|
||||
<p class="mat-body">Optional values used as defaults during device setup.</p>
|
||||
|
||||
<h2 class="mat-h2">Geographical Location</h2>
|
||||
<div fxLayout="row wrap" fxLayoutAlign="space-around" fxLayout.xs="column">
|
||||
<account-country-input fxFlex.gt-xs="40"
|
||||
[countries$]="countries$"
|
||||
[deviceForm]="defaultsForm"
|
||||
[required]="false"
|
||||
(countrySelected)="onCountrySelect($event)"
|
||||
>
|
||||
</account-country-input>
|
||||
<account-region-input fxFlex.gt-xs="40"
|
||||
[regions$]="regions$"
|
||||
[deviceForm]="defaultsForm"
|
||||
[required]="false"
|
||||
(regionSelected)="onRegionSelect($event)"
|
||||
>
|
||||
</account-region-input>
|
||||
<account-city-input fxFlex.gt-xs="40"
|
||||
[cities$]="cities$"
|
||||
[deviceForm]="defaultsForm"
|
||||
[required]="false"
|
||||
(citySelected)="onCitySelect($event)"
|
||||
>
|
||||
</account-city-input>
|
||||
<account-timezone-input fxFlex.gt-xs="40"
|
||||
[timezones$]="timezones$"
|
||||
[deviceForm]="defaultsForm"
|
||||
[required]="false"
|
||||
>
|
||||
</account-timezone-input>
|
||||
</div>
|
||||
|
||||
<h2 class="mat-h2">Voice</h2>
|
||||
<account-option-buttons
|
||||
[config]="voiceOptionsConfig"
|
||||
[selectedOption]="defaultsForm.controls['voice'].value"
|
||||
(selectionChange)="changeVoice($event)"
|
||||
>
|
||||
</account-option-buttons>
|
||||
|
||||
<h2 class="mat-h2">Wake Word</h2>
|
||||
<account-option-buttons
|
||||
[config]="wakeWordOptionsConfig"
|
||||
[selectedOption]="defaultsForm.controls['wakeWord'].value"
|
||||
(selectionChange)="changeWakeWord($event)"
|
||||
>
|
||||
</account-option-buttons>
|
||||
</mat-card-content>
|
||||
<mat-card-actions align="right" *ngIf="!deviceSetup">
|
||||
<button mat-button [disabled]="!defaultsForm.valid" (click)="onSave()">SAVE</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { City } from '@account/models/city.model';
|
||||
import { Country } from '@account/models/country.model';
|
||||
import { DeviceService } from '@account/http/device.service';
|
||||
import { GeographyService } from '@account/http/geography_service';
|
||||
import { OptionButtonsConfig } from '@account/models/option-buttons-config.model';
|
||||
import { Region } from '@account/models/region.model';
|
||||
import { Timezone } from '@account/models/timezone.model';
|
||||
|
||||
@Component({
|
||||
selector: 'account-defaults',
|
||||
templateUrl: './defaults.component.html',
|
||||
styleUrls: ['./defaults.component.scss']
|
||||
})
|
||||
export class DefaultsComponent implements OnInit {
|
||||
@Input() deviceSetup: boolean;
|
||||
public cities$ = new Observable<City[]>();
|
||||
public countries$ = new Observable<Country[]>();
|
||||
@Input() defaultsForm: FormGroup;
|
||||
public regions$ = new Observable<Region[]>();
|
||||
public timezones$ = new Observable<Timezone[]>();
|
||||
public voiceOptionsConfig: OptionButtonsConfig;
|
||||
public wakeWordOptionsConfig: OptionButtonsConfig;
|
||||
|
||||
constructor(
|
||||
private deviceService: DeviceService,
|
||||
private formBuilder: FormBuilder,
|
||||
private geoService: GeographyService
|
||||
) {
|
||||
this.voiceOptionsConfig = {
|
||||
options: ['British Male', 'American Female', 'American Male', 'Google Voice'],
|
||||
buttonWidth: '140px'
|
||||
};
|
||||
this.wakeWordOptionsConfig = {
|
||||
options: ['Hey Mycroft', 'Christopher', 'Hey Ezra', 'Hey Jarvis'],
|
||||
buttonWidth: '130px'
|
||||
};
|
||||
this.defaultsForm = this.formBuilder.group(
|
||||
{
|
||||
name: [null]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.countries$ = this.geoService.getCountries();
|
||||
}
|
||||
|
||||
onCountrySelect(selectedCountry: Country): void {
|
||||
if (selectedCountry) {
|
||||
this.defaultsForm.controls['region'].enable();
|
||||
this.defaultsForm.controls['timezone'].enable();
|
||||
this.regions$ = this.geoService.getRegionsByCountry(selectedCountry);
|
||||
this.timezones$ = this.geoService.getTimezonesByCountry(selectedCountry);
|
||||
} else {
|
||||
this.defaultsForm.controls['region'].disable();
|
||||
this.defaultsForm.controls['region'].setValue('');
|
||||
this.defaultsForm.controls['timezone'].disable();
|
||||
this.defaultsForm.controls['timezone'].setValue('');
|
||||
}
|
||||
}
|
||||
|
||||
onRegionSelect(selectedRegion: Region): void {
|
||||
if (selectedRegion) {
|
||||
this.defaultsForm.controls['city'].enable();
|
||||
this.cities$ = this.geoService.getCitiesByRegion(selectedRegion);
|
||||
} else {
|
||||
this.defaultsForm.controls['city'].disable();
|
||||
this.defaultsForm.controls['city'].setValue('');
|
||||
}
|
||||
}
|
||||
|
||||
onCitySelect(selectedCity: City): void {
|
||||
if (selectedCity) {
|
||||
this.defaultsForm.controls['timezone'].setValue(selectedCity.timezone);
|
||||
}
|
||||
}
|
||||
|
||||
changeVoice(newValue: string) {
|
||||
this.defaultsForm.patchValue({voice: newValue});
|
||||
}
|
||||
|
||||
changeWakeWord(newValue: string) {
|
||||
this.defaultsForm.patchValue({wakeWord: newValue});
|
||||
}
|
||||
|
||||
onSave() {
|
||||
if (this.deviceSetup) {
|
||||
this.deviceService.addAccountDefaults(this.defaultsForm).subscribe();
|
||||
} else {
|
||||
this.deviceService.updateAccountDefaults(this.defaultsForm).subscribe();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Device } from '@account/models/device.model';
|
||||
|
||||
@Component({
|
||||
selector: 'account-device-info',
|
||||
templateUrl: './device-info.component.html',
|
||||
styleUrls: ['./device-info.component.scss']
|
||||
})
|
||||
export class DeviceInfoComponent implements OnInit {
|
||||
@Input() device: Device;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
<div id="add-device-button" fxLayout="row" fxLayoutAlign="start center" routerLink="/devices/add">
|
||||
<img src="../assets/generic-device-icon-blue.svg">
|
||||
<span fxFlex class="mat-h2">ADD DEVICE</span>
|
||||
<fa-icon class="mat-h2" [icon]="addIcon"></fa-icon>
|
||||
</div>
|
||||
|
||||
<div fxLayout="row wrap" fxLayoutGap.gt-xs="16px">
|
||||
<mat-card *ngFor="let device of devices">
|
||||
<mat-card-title fxLayout="row" fxLayoutAlign="start center">
|
||||
<img [src]="getDeviceIcon(device)"/>
|
||||
{{device.name}}
|
||||
</mat-card-title>
|
||||
<account-device-info [device]="device"></account-device-info>
|
||||
<!--<mat-card-actions>-->
|
||||
<!--<button mat-flat-button color="warn" (click)="onRemovalClick(device)">-->
|
||||
<!--REMOVE-->
|
||||
<!--</button>-->
|
||||
<!--<button mat-flat-button color="primary" (click)="onDeviceEdit(device)">-->
|
||||
<!--EDIT-->
|
||||
<!--</button>-->
|
||||
<!--</mat-card-actions>-->
|
||||
</mat-card>
|
||||
</div>
|
|
@ -1,43 +0,0 @@
|
|||
<mat-card class="mat-elevation-z0" id="required-settings-card" [formGroup]="preferencesForm">
|
||||
<mat-card-title *ngIf="deviceSetup">Setup your device preferences</mat-card-title>
|
||||
<mat-card-title *ngIf="!deviceSetup">Manage your device preferences</mat-card-title>
|
||||
<mat-card-content fxLayout="column">
|
||||
<p class="mat-body">
|
||||
Preferences are applied to all your devices to present information
|
||||
in a manner you are accustomed to.
|
||||
</p>
|
||||
<account-option-buttons
|
||||
[config]="measurementOptionsConfig"
|
||||
[selectedOption]="preferencesForm.controls['measurementSystem'].value"
|
||||
(selectionChange)="changeMeasurementSystem($event)"
|
||||
>
|
||||
</account-option-buttons>
|
||||
<account-option-buttons
|
||||
[config]="timeFormatOptionsConfig"
|
||||
[selectedOption]="preferencesForm.controls['timeFormat'].value"
|
||||
(selectionChange)="changeTimeFormat($event)"
|
||||
>
|
||||
</account-option-buttons>
|
||||
<account-option-buttons
|
||||
[config]="dateFormatOptionsConfig"
|
||||
[selectedOption]="preferencesForm.controls['dateFormat'].value"
|
||||
(selectionChange)="changeDateFormat($event)"
|
||||
>
|
||||
</account-option-buttons>
|
||||
</mat-card-content>
|
||||
<mat-card-actions align="right" *ngIf="!deviceSetup">
|
||||
<button mat-button (click)="onSave()" [disabled]="!preferencesForm.valid">SAVE</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
|
||||
<!--<mat-card *ngIf="!deviceSetup" id="advanced-settings-card">-->
|
||||
<!--<mat-toolbar>-->
|
||||
<!--<span class="section-card-title">Advanced Settings</span>-->
|
||||
<!--</mat-toolbar>-->
|
||||
<!--<div fxLayout="column" class="section-content">-->
|
||||
<!--<div class="mat-body" *ngFor="let paragraph of advancedSettingsDesc">-->
|
||||
<!--<p>{{paragraph}}</p>-->
|
||||
<!--</div>-->
|
||||
<!--<button mat-flat-button>VIEW DOCUMENTATION</button>-->
|
||||
<!--</div>-->
|
||||
<!--</mat-card>-->
|
|
@ -1,36 +0,0 @@
|
|||
@import "~@angular/material/theming";
|
||||
@import "mycroft-colors";
|
||||
@import "components/buttons";
|
||||
@import "components/cards";
|
||||
|
||||
mat-card {
|
||||
@include section-card;
|
||||
max-width: 700px;
|
||||
|
||||
mat-card-title {
|
||||
color: mat-color($mycroft-primary)
|
||||
}
|
||||
}
|
||||
|
||||
#required-settings-card {
|
||||
margin-top: 32px;
|
||||
|
||||
button {
|
||||
@include action-button-primary;
|
||||
margin: 16px;
|
||||
|
||||
&:disabled {
|
||||
background-color: mat-color($mycroft-accent, 200);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#advanced-settings-card {
|
||||
button {
|
||||
@include action-button-primary;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
<!--
|
||||
Editable device attribute fields
|
||||
|
||||
There are a bunch of conditional fields in this form because it is re-used for four
|
||||
different purposes. Not all of the use cases need all the fields. However, the forms are
|
||||
similar enough that it made sense to include all four use cases in a single component to avoid
|
||||
a bunch of duplicate code.
|
||||
-->
|
||||
<mat-card class="mat-elevation-z0" id="default-settings-card" [formGroup]="deviceForm">
|
||||
<ng-container [ngSwitch]="action">
|
||||
<mat-card-title *ngSwitchCase="'default setup'">Setup your device defaults</mat-card-title>
|
||||
<mat-card-title *ngSwitchCase="'default edit'">Manage your device defaults</mat-card-title>
|
||||
<mat-card-title *ngSwitchCase="'device setup'">Configure your new device</mat-card-title>
|
||||
<mat-card-title *ngSwitchDefault>Change device configuration</mat-card-title>
|
||||
</ng-container>
|
||||
|
||||
<!-- Device name and placement -->
|
||||
<mat-card-content
|
||||
*ngIf="action.startsWith('device')"
|
||||
fxLayout="column"
|
||||
fxLayoutAlign="space-around"
|
||||
fxLayout.xs="column"
|
||||
>
|
||||
<mat-form-field *ngIf="action === 'device setup'" [appearance]="'outline'">
|
||||
<mat-label>Pairing Code</mat-label>
|
||||
<input
|
||||
matInput
|
||||
required
|
||||
type="text"
|
||||
onInput="this.value = this.value.toUpperCase()"
|
||||
formControlName="pairingCode"
|
||||
>
|
||||
<mat-hint>Code spoken by device</mat-hint>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlex.gt-xs="40" [appearance]="'outline'">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput required type="text" formControlName="name">
|
||||
<mat-hint>Must be unique</mat-hint>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlex.gt-xs="40" [appearance]="'outline'">
|
||||
<mat-label>Placement</mat-label>
|
||||
<input matInput type="text" formControlName="placement">
|
||||
<mat-hint>e.g. Kitchen, Bedroom, Office</mat-hint>
|
||||
</mat-form-field>
|
||||
</mat-card-content>
|
||||
|
||||
<!-- Geographical location -->
|
||||
<mat-card-content>
|
||||
<p *ngIf="action.startsWith('default')" class="mat-body">
|
||||
Optional values used as defaults during device setup.
|
||||
</p>
|
||||
|
||||
<h2 class="mat-h2">Geographical Location</h2>
|
||||
<div fxLayout="row wrap" fxLayoutAlign="space-around" fxLayout.xs="column">
|
||||
<account-country-input fxFlex.gt-xs="40"
|
||||
[countries$]="countries$"
|
||||
[deviceForm]="deviceForm"
|
||||
[required]="action.startsWith('device')"
|
||||
(countrySelected)="onCountrySelect($event)"
|
||||
>
|
||||
</account-country-input>
|
||||
<account-region-input fxFlex.gt-xs="40"
|
||||
[regions$]="regions$"
|
||||
[deviceForm]="deviceForm"
|
||||
[required]="action.startsWith('device')"
|
||||
(regionSelected)="onRegionSelect($event)"
|
||||
>
|
||||
</account-region-input>
|
||||
<account-city-input fxFlex.gt-xs="40"
|
||||
[cities$]="cities$"
|
||||
[deviceForm]="deviceForm"
|
||||
[required]="action.startsWith('device')"
|
||||
(citySelected)="onCitySelect($event)"
|
||||
>
|
||||
</account-city-input>
|
||||
<account-timezone-input fxFlex.gt-xs="40"
|
||||
[timezones$]="timezones$"
|
||||
[deviceForm]="deviceForm"
|
||||
[required]="action.startsWith('device')"
|
||||
>
|
||||
</account-timezone-input>
|
||||
</div>
|
||||
|
||||
<h2 class="mat-h2">Voice</h2>
|
||||
<account-option-buttons
|
||||
[config]="voiceOptionsConfig"
|
||||
[selectedOption]="deviceForm.controls['voice'].value"
|
||||
(selectionChange)="changeVoice($event)"
|
||||
>
|
||||
</account-option-buttons>
|
||||
|
||||
<h2 class="mat-h2">Wake Word</h2>
|
||||
<account-option-buttons
|
||||
[config]="wakeWordOptionsConfig"
|
||||
[selectedOption]="deviceForm.controls['wakeWord'].value"
|
||||
(selectionChange)="changeWakeWord($event)"
|
||||
>
|
||||
</account-option-buttons>
|
||||
</mat-card-content>
|
||||
<mat-card-actions align="right" *ngIf="action === 'default edit'">
|
||||
<button mat-button [disabled]="!deviceForm.valid" (click)="onSave()">SAVE</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { AccountDefaults } from '../../../shared/models/defaults.model';
|
||||
import { City } from '../../../shared/models/city.model';
|
||||
import { Country } from '../../../shared/models/country.model';
|
||||
import { DeviceService } from '../../../core/http/device.service';
|
||||
import { GeographyService } from '../../../core/http/geography_service';
|
||||
import { OptionButtonsConfig } from '../../../shared/models/option-buttons-config.model';
|
||||
import { Region } from '../../../shared/models/region.model';
|
||||
import { Timezone } from '../../../shared/models/timezone.model';
|
||||
|
||||
@Component({
|
||||
selector: 'account-device-edit',
|
||||
templateUrl: './device-edit.component.html',
|
||||
styleUrls: ['./device-edit.component.scss']
|
||||
})
|
||||
export class DeviceEditComponent implements OnInit {
|
||||
@Input() action: string;
|
||||
public cities$ = new Observable<City[]>();
|
||||
public countries$ = new Observable<Country[]>();
|
||||
@Input() deviceForm: FormGroup;
|
||||
@Input() defaults: AccountDefaults;
|
||||
public regions$ = new Observable<Region[]>();
|
||||
public timezones$ = new Observable<Timezone[]>();
|
||||
public voiceOptionsConfig: OptionButtonsConfig;
|
||||
public wakeWordOptionsConfig: OptionButtonsConfig;
|
||||
|
||||
constructor(
|
||||
private deviceService: DeviceService,
|
||||
private formBuilder: FormBuilder,
|
||||
private geoService: GeographyService
|
||||
) {
|
||||
this.voiceOptionsConfig = {
|
||||
options: ['British Male', 'American Female', 'American Male', 'Google Voice'],
|
||||
buttonWidth: '140px'
|
||||
};
|
||||
this.wakeWordOptionsConfig = {
|
||||
options: ['Hey Mycroft', 'Christopher', 'Hey Ezra', 'Hey Jarvis'],
|
||||
buttonWidth: '130px'
|
||||
};
|
||||
this.deviceForm = this.formBuilder.group(
|
||||
{
|
||||
name: [null]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
// if (!this.deviceForm) {
|
||||
// this.deviceForm = this.formBuilder.group(
|
||||
// {
|
||||
// name: [this.deviceService.selectedDevice.name]
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
// Disable the controls that depend on other control values to be pre-populated.
|
||||
this.deviceForm.controls['region'].disable();
|
||||
this.deviceForm.controls['city'].disable();
|
||||
this.deviceForm.controls['timezone'].disable();
|
||||
this.countries$ = this.geoService.getCountries();
|
||||
if (this.action === 'device setup' && this.defaults) {
|
||||
this.applyDefaultValues();
|
||||
}
|
||||
}
|
||||
|
||||
applyDefaultValues() {
|
||||
if (this.defaults.country) {
|
||||
this.deviceForm.controls['country'].setValue(
|
||||
this.defaults.country.name
|
||||
);
|
||||
}
|
||||
if (this.defaults.region) {
|
||||
this.deviceForm.controls['region'].setValue(
|
||||
this.defaults.region.name
|
||||
);
|
||||
}
|
||||
if (this.defaults.city) {
|
||||
this.deviceForm.controls['city'].setValue(
|
||||
this.defaults.city.name
|
||||
);
|
||||
}
|
||||
if (this.defaults.timezone) {
|
||||
this.deviceForm.controls['timezone'].setValue(
|
||||
this.defaults.timezone.name
|
||||
);
|
||||
}
|
||||
if (this.defaults.voice) {
|
||||
this.deviceForm.controls['voice'].setValue(
|
||||
this.defaults.voice.displayName
|
||||
);
|
||||
}
|
||||
if (this.defaults.wakeWord) {
|
||||
this.deviceForm.controls['wakeWord'].setValue(
|
||||
this.defaults.wakeWord.displayName
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onCountrySelect(selectedCountry: Country): void {
|
||||
if (selectedCountry) {
|
||||
this.deviceForm.controls['region'].enable();
|
||||
this.deviceForm.controls['timezone'].enable();
|
||||
this.regions$ = this.geoService.getRegionsByCountry(selectedCountry);
|
||||
this.timezones$ = this.geoService.getTimezonesByCountry(selectedCountry);
|
||||
} else {
|
||||
this.deviceForm.controls['region'].disable();
|
||||
this.deviceForm.controls['region'].setValue('');
|
||||
this.deviceForm.controls['timezone'].disable();
|
||||
this.deviceForm.controls['timezone'].setValue('');
|
||||
}
|
||||
}
|
||||
|
||||
onRegionSelect(selectedRegion: Region): void {
|
||||
if (selectedRegion) {
|
||||
this.deviceForm.controls['city'].enable();
|
||||
this.cities$ = this.geoService.getCitiesByRegion(selectedRegion);
|
||||
} else {
|
||||
this.deviceForm.controls['city'].disable();
|
||||
this.deviceForm.controls['city'].setValue('');
|
||||
}
|
||||
}
|
||||
|
||||
onCitySelect(selectedCity: City): void {
|
||||
if (selectedCity) {
|
||||
this.deviceForm.controls['timezone'].setValue(selectedCity.timezone);
|
||||
}
|
||||
}
|
||||
|
||||
changeVoice(newValue: string) {
|
||||
this.deviceForm.patchValue({voice: newValue});
|
||||
}
|
||||
|
||||
changeWakeWord(newValue: string) {
|
||||
this.deviceForm.patchValue({wakeWord: newValue});
|
||||
}
|
||||
|
||||
onSave() {
|
||||
if (this.defaults) {
|
||||
this.deviceService.updateAccountDefaults(this.deviceForm).subscribe(
|
||||
() => { this.defaults = this.deviceForm.value; }
|
||||
);
|
||||
} else {
|
||||
this.deviceService.addAccountDefaults(this.deviceForm).subscribe(
|
||||
() => { this.defaults = this.deviceForm.value; }
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,32 +2,53 @@ import { NgModule } from '@angular/core';
|
|||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { DefaultsResolverService } from '../../core/guards/defaults-resolver.service';
|
||||
import { DeviceAddComponent } from './pages/device-add/device-add.component';
|
||||
import { DevicesComponent } from './pages/devices/devices.component';
|
||||
import { DeviceEditComponent } from './device-edit/device-edit.component';
|
||||
import { AddComponent } from './pages/add/add.component';
|
||||
import { DeviceComponent } from './device.component';
|
||||
import { DefaultsComponent } from '@account/app/modules/device/pages/defaults/defaults.component';
|
||||
import { DeviceEditComponent } from '@account/app/modules/device/pages/device-edit/device-edit.component';
|
||||
import { DeviceListComponent } from './pages/device-list/device-list.component';
|
||||
import { DeviceResolverService } from '../../core/guards/device-resolver.service';
|
||||
import { PreferencesComponent } from './pages/preferences/preferences.component';
|
||||
import { PreferencesResolverService } from '../../core/guards/preferences-resolver.service';
|
||||
|
||||
const deviceRoutes: Routes = [
|
||||
{
|
||||
path: 'devices',
|
||||
component: DevicesComponent,
|
||||
resolve: {
|
||||
defaults: DefaultsResolverService,
|
||||
devices: DeviceResolverService,
|
||||
preferences: PreferencesResolverService
|
||||
}
|
||||
component: DeviceComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: DeviceListComponent,
|
||||
resolve: {
|
||||
devices: DeviceResolverService,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'preferences',
|
||||
component: PreferencesComponent,
|
||||
resolve: {
|
||||
preferences: PreferencesResolverService,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'defaults',
|
||||
component: DefaultsComponent,
|
||||
resolve: {
|
||||
defaults: DefaultsResolverService
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'devices/add',
|
||||
component: DeviceAddComponent,
|
||||
component: AddComponent,
|
||||
resolve: {
|
||||
defaults: DefaultsResolverService,
|
||||
preferences: PreferencesResolverService
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'devices/:device_id',
|
||||
path: 'devices/:deviceId',
|
||||
component: DeviceEditComponent,
|
||||
}
|
||||
];
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<nav mat-tab-nav-bar fxLayout="row" fxLayoutAlign="center">
|
||||
<a mat-tab-link [routerLink]="'./'">Devices</a>
|
||||
<a mat-tab-link [routerLink]="'./preferences'">Preferences</a>
|
||||
<a mat-tab-link [routerLink]="'./defaults'">Defaults</a>
|
||||
</nav>
|
||||
|
||||
<router-outlet></router-outlet>
|
|
@ -0,0 +1,3 @@
|
|||
nav {
|
||||
margin-bottom: 32px;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'account-device',
|
||||
templateUrl: './device.component.html',
|
||||
styleUrls: ['./device.component.scss']
|
||||
})
|
||||
export class DeviceComponent implements OnInit {
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
}
|
|
@ -10,7 +10,6 @@ import {
|
|||
MatExpansionModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatRadioModule,
|
||||
MatStepperModule,
|
||||
MatTabsModule,
|
||||
MatToolbarModule
|
||||
|
@ -18,42 +17,54 @@ import {
|
|||
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
|
||||
import { AddCompleteComponent } from './components/view/add-complete/add-complete.component';
|
||||
import { AddCompleteComponent } from './components/card/add-complete/add-complete.component';
|
||||
import { AddComponent } from './pages/add/add.component';
|
||||
import { CityInputComponent } from './components/input/city-input/city-input.component';
|
||||
import { CountryInputComponent } from './components/input/country-input/country-input.component';
|
||||
import { DefaultsComponent } from './components/view/defaults/defaults.component';
|
||||
import { DeviceAddComponent } from './pages/device-add/device-add.component';
|
||||
import { DeviceEditComponent } from './device-edit/device-edit.component';
|
||||
import { DeviceInfoComponent } from './components/view/device-info/device-info.component';
|
||||
import { DeviceListComponent } from './components/view/device-list/device-list.component';
|
||||
import { DefaultsCardComponent } from './components/card/defaults-card/defaults-card.component';
|
||||
import { DefaultsComponent } from './pages/defaults/defaults.component';
|
||||
import { DeviceComponent } from '@account/app/modules/device/device.component';
|
||||
import { DeviceEditCardComponent } from './components/card/device-edit-card/device-edit-card.component';
|
||||
import { DeviceEditComponent } from './pages/device-edit/device-edit.component';
|
||||
import { DeviceDisplayComponent } from './components/card/device-display/device-display.component';
|
||||
import { DeviceListComponent } from './pages/device-list/device-list.component';
|
||||
import { DeviceRoutingModule } from './device-routing.module';
|
||||
import { DevicesComponent } from './pages/devices/devices.component';
|
||||
import { DeviceService } from '@account/http/device.service';
|
||||
import { PreferencesComponent } from './components/view/preferences/preferences.component';
|
||||
import { GeographyCardComponent } from './components/card/geography-card/geography-card.component';
|
||||
import { PreferencesComponent } from './pages/preferences/preferences.component';
|
||||
import { RegionInputComponent } from './components/input/region-input/region-input.component';
|
||||
import { RemoveComponent } from './remove/remove.component';
|
||||
import { RemoveDeviceDialogComponent } from './components/modal/remove-device-dialog/remove-device-dialog.component';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { TimezoneInputComponent } from './components/input/timezone-input/timezone-input.component';
|
||||
import { PreferencesCardComponent } from './components/card/preferences-card/preferences-card.component';
|
||||
import { VoiceCardComponent } from './components/card/voice-card/voice-card.component';
|
||||
import { WakeWordCardComponent } from './components/card/wake-word-card/wake-word-card.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddCompleteComponent,
|
||||
AddComponent,
|
||||
CityInputComponent,
|
||||
CountryInputComponent,
|
||||
DefaultsCardComponent,
|
||||
DefaultsComponent,
|
||||
DeviceAddComponent,
|
||||
DeviceComponent,
|
||||
DeviceEditCardComponent,
|
||||
DeviceEditComponent,
|
||||
DeviceInfoComponent,
|
||||
DeviceDisplayComponent,
|
||||
DeviceListComponent,
|
||||
DevicesComponent,
|
||||
GeographyCardComponent,
|
||||
PreferencesCardComponent,
|
||||
PreferencesComponent,
|
||||
RegionInputComponent,
|
||||
RemoveComponent,
|
||||
RemoveDeviceDialogComponent,
|
||||
TimezoneInputComponent,
|
||||
VoiceCardComponent,
|
||||
WakeWordCardComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
DeviceAddComponent,
|
||||
RemoveComponent
|
||||
DeviceComponent,
|
||||
RemoveDeviceDialogComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -68,7 +79,6 @@ import { TimezoneInputComponent } from './components/input/timezone-input/timezo
|
|||
MatExpansionModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatRadioModule,
|
||||
MatStepperModule,
|
||||
MatTabsModule,
|
||||
MatToolbarModule,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<!-- Show a horizontal stepper on larger devices -->
|
||||
<mat-horizontal-stepper *ngIf="!alignVertical" labelPosition="bottom">
|
||||
<mat-horizontal-stepper *ngIf="!alignVertical" labelPosition="bottom" [linear]="true">
|
||||
<!-- Use font awesome icons in the stepper to indicate progress -->
|
||||
<ng-template matStepperIcon="done">
|
||||
<fa-icon [icon]="stepDoneIcon"></fa-icon>
|
||||
|
@ -10,11 +10,8 @@
|
|||
|
||||
<mat-step label="Preferences" *ngIf="!preferences" [stepControl]="preferencesForm">
|
||||
<form [formGroup]="preferencesForm" (ngSubmit)="onPreferencesSubmit()">
|
||||
<account-device-preferences
|
||||
[preferencesForm]="preferencesForm"
|
||||
[deviceSetup]="true"
|
||||
>
|
||||
</account-device-preferences>
|
||||
<account-preferences-card [addingDevice]="true" [preferencesForm]="preferencesForm">
|
||||
</account-preferences-card>
|
||||
<div fxLayout="row" fxLayoutAlign="end">
|
||||
<button mat-button matStepperNext [disabled]="!preferencesForm.valid">
|
||||
NEXT
|
||||
|
@ -25,11 +22,12 @@
|
|||
|
||||
<mat-step label="Defaults" *ngIf="!preferences" [stepControl]="defaultsForm">
|
||||
<form [formGroup]="defaultsForm" (ngSubmit)="onDefaultsSubmit()">
|
||||
<account-device-edit
|
||||
[deviceForm]="defaultsForm"
|
||||
[action]="'default setup'"
|
||||
<account-defaults-card
|
||||
[addingDevice]="true"
|
||||
[defaults]="defaults"
|
||||
[defaultsForm]="defaultsForm"
|
||||
>
|
||||
</account-device-edit>
|
||||
</account-defaults-card>
|
||||
<div fxLayout="row" fxLayoutAlign="end">
|
||||
<button mat-button matStepperNext [disabled]="!defaultsForm.valid">
|
||||
NEXT
|
||||
|
@ -40,12 +38,11 @@
|
|||
|
||||
<mat-step label="Device" [stepControl]="deviceForm">
|
||||
<form [formGroup]="deviceForm" (ngSubmit)="onDeviceSubmit()">
|
||||
<account-device-edit
|
||||
<account-device-edit-card
|
||||
[deviceForm]="deviceForm"
|
||||
[action]="'device setup'"
|
||||
[defaults]="defaults"
|
||||
[addDevice]="true"
|
||||
>
|
||||
</account-device-edit>
|
||||
</account-device-edit-card>
|
||||
<div fxLayout="row" fxLayoutAlign="end">
|
||||
<button mat-button matStepperNext [disabled]="!deviceForm.valid">
|
||||
NEXT
|
||||
|
@ -64,7 +61,7 @@
|
|||
</mat-horizontal-stepper>
|
||||
|
||||
<!-- Show a vertical stepper on smaller devices -->
|
||||
<mat-vertical-stepper *ngIf="alignVertical">
|
||||
<mat-vertical-stepper *ngIf="alignVertical" [linear]="true">
|
||||
<!-- Use font awesome icons in the stepper to indicate progress -->
|
||||
<ng-template matStepperIcon="done">
|
||||
<fa-icon [icon]="stepDoneIcon"></fa-icon>
|
||||
|
@ -75,11 +72,9 @@
|
|||
|
||||
<mat-step label="Preferences" *ngIf="!preferences" [stepControl]="preferencesForm">
|
||||
<form [formGroup]="preferencesForm" (ngSubmit)="onPreferencesSubmit()">
|
||||
<account-device-preferences
|
||||
[preferencesForm]="preferencesForm"
|
||||
[deviceSetup]="true"
|
||||
>
|
||||
</account-device-preferences>
|
||||
<account-preferences-card [preferencesForm]="preferencesForm">
|
||||
<mat-card-title>Setup your device preferences</mat-card-title>
|
||||
</account-preferences-card>
|
||||
<div fxLayout="row" fxLayoutAlign="end">
|
||||
<button mat-button matStepperNext [disabled]="!preferencesForm.valid">
|
||||
NEXT
|
||||
|
@ -90,11 +85,12 @@
|
|||
|
||||
<mat-step label="Defaults" *ngIf="!preferences" [stepControl]="defaultsForm">
|
||||
<form [formGroup]="defaultsForm" (ngSubmit)="onDefaultsSubmit()">
|
||||
<account-device-edit
|
||||
[deviceForm]="defaultsForm"
|
||||
[action]="'default setup'"
|
||||
<account-defaults-card
|
||||
[addingDevice]="true"
|
||||
[defaults]="defaults"
|
||||
[defaultsForm]="defaultsForm"
|
||||
>
|
||||
</account-device-edit>
|
||||
</account-defaults-card>
|
||||
<div fxLayout="row" fxLayoutAlign="end">
|
||||
<button mat-button matStepperNext [disabled]="!defaultsForm.valid">
|
||||
NEXT
|
||||
|
@ -105,12 +101,11 @@
|
|||
|
||||
<mat-step label="Device" [stepControl]="deviceForm">
|
||||
<form [formGroup]="deviceForm" (ngSubmit)="onDeviceSubmit()">
|
||||
<account-device-edit
|
||||
<account-device-edit-card
|
||||
[deviceForm]="deviceForm"
|
||||
[action]="'device setup'"
|
||||
[defaults]="defaults"
|
||||
[addDevice]="true"
|
||||
>
|
||||
</account-device-edit>
|
||||
</account-device-edit-card>
|
||||
<div fxLayout="row" fxLayoutAlign="end">
|
||||
<button mat-button matStepperNext [disabled]="!deviceForm.valid">
|
||||
NEXT
|
|
@ -0,0 +1,17 @@
|
|||
@import "~@angular/material/theming";
|
||||
@import "mycroft-colors";
|
||||
@import "components/buttons";
|
||||
|
||||
mat-card-title {
|
||||
color: mat-color($mycroft-primary)
|
||||
}
|
||||
|
||||
button {
|
||||
@include action-button-primary;
|
||||
}
|
||||
|
||||
mat-horizontal-stepper {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 800px;
|
||||
}
|
|
@ -1,21 +1,40 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { MediaChange, MediaObserver } from '@angular/flex-layout';
|
||||
import {FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import {
|
||||
AbstractControl,
|
||||
AsyncValidatorFn,
|
||||
FormBuilder,
|
||||
FormGroup,
|
||||
ValidationErrors,
|
||||
Validators
|
||||
} from '@angular/forms';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { catchError, map, tap } from 'rxjs/operators';
|
||||
|
||||
import { AccountDefaults } from '@account/models/defaults.model';
|
||||
import { AccountPreferences } from '@account/models/preferences.model';
|
||||
import { DeviceService } from '@account/http/device.service';
|
||||
|
||||
|
||||
export function pairingCodeValidator(deviceService: DeviceService): AsyncValidatorFn {
|
||||
return (control: AbstractControl): Observable<ValidationErrors | null> => {
|
||||
return deviceService.validatePairingCode(control.value).pipe(
|
||||
map((response) => response.isValid ? null : { unknownPairingCode: true }),
|
||||
catchError(() => null),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'account-device-add',
|
||||
templateUrl: './device-add.component.html',
|
||||
styleUrls: ['./device-add.component.scss']
|
||||
selector: 'account-device-add',
|
||||
templateUrl: './add.component.html',
|
||||
styleUrls: ['./add.component.scss']
|
||||
})
|
||||
export class DeviceAddComponent implements OnInit {
|
||||
export class AddComponent implements OnInit {
|
||||
public alignVertical: boolean;
|
||||
public defaults: AccountDefaults;
|
||||
public defaultsForm: FormGroup;
|
||||
|
@ -52,16 +71,10 @@ export class DeviceAddComponent implements OnInit {
|
|||
}
|
||||
);
|
||||
|
||||
// The defaults and device forms must have the same fields because they
|
||||
// both use the same component. The name, pairing code and placement
|
||||
// controls are only placeholders in this context to facilitate form re-use.
|
||||
this.defaultsForm = this.formBuilder.group(
|
||||
{
|
||||
city: [null],
|
||||
country: [null],
|
||||
name: [null],
|
||||
pairingCode: [null],
|
||||
placement: [null],
|
||||
region: [null],
|
||||
timezone: [null],
|
||||
wakeWord: [null],
|
||||
|
@ -70,22 +83,23 @@ export class DeviceAddComponent implements OnInit {
|
|||
);
|
||||
this.deviceForm = this.formBuilder.group(
|
||||
{
|
||||
city: [null, Validators.required],
|
||||
city: [this.defaults ? this.defaults.city.name : null, Validators.required],
|
||||
name: [null, Validators.required],
|
||||
country: [null, Validators.required],
|
||||
country: [this.defaults ? this.defaults.country.name : null, Validators.required],
|
||||
pairingCode: [
|
||||
null,
|
||||
[
|
||||
Validators.required,
|
||||
Validators.maxLength(6),
|
||||
Validators.minLength(6)
|
||||
]
|
||||
],
|
||||
[ pairingCodeValidator(this.deviceService) ],
|
||||
],
|
||||
placement: [null],
|
||||
region: [null, Validators.required],
|
||||
timezone: [null, Validators.required],
|
||||
wakeWord: [null, Validators.required],
|
||||
voice: [null, Validators.required]
|
||||
region: [this.defaults ? this.defaults.region.name : null, Validators.required],
|
||||
timezone: [this.defaults ? this.defaults.timezone.name : null, Validators.required],
|
||||
wakeWord: [this.defaults ? this.defaults.wakeWord.displayName : null, Validators.required],
|
||||
voice: [this.defaults ? this.defaults.voice.displayName : null, Validators.required]
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<account-defaults-card
|
||||
[addingDevice]="false"
|
||||
[defaults]="defaults"
|
||||
[defaultsForm]="defaultsForm"
|
||||
>
|
||||
</account-defaults-card>
|
|
@ -0,0 +1,35 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { AccountDefaults } from '@account/models/defaults.model';
|
||||
|
||||
@Component({
|
||||
selector: 'account-defaults',
|
||||
templateUrl: './defaults.component.html',
|
||||
styleUrls: ['./defaults.component.scss']
|
||||
})
|
||||
export class DefaultsComponent implements OnInit {
|
||||
public defaults: AccountDefaults;
|
||||
public defaultsForm: FormGroup;
|
||||
|
||||
constructor(private formBuilder: FormBuilder, private route: ActivatedRoute) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe(
|
||||
(data: {defaults: AccountDefaults}) => { this.defaults = data.defaults; }
|
||||
);
|
||||
this.defaultsForm = this.formBuilder.group(
|
||||
{
|
||||
city: [this.defaults ? this.defaults.city.name : null],
|
||||
country: [this.defaults ? this.defaults.country.name : null],
|
||||
region: [this.defaults ? this.defaults.region.name : null],
|
||||
timezone: [this.defaults ? this.defaults.timezone.name : null],
|
||||
wakeWord: [this.defaults ? this.defaults.wakeWord.displayName : null],
|
||||
voice: [this.defaults ? this.defaults.voice.displayName : null]
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<account-device-edit-card
|
||||
*ngIf="device$ | async"
|
||||
[deviceForm]="deviceForm"
|
||||
[addDevice]="false"
|
||||
(saveChanges)="onExit($event)"
|
||||
>
|
||||
</account-device-edit-card>
|
||||
|
||||
<mat-card id="advanced-settings-card">
|
||||
<mat-card-title>Advanced Settings</mat-card-title>
|
||||
<mat-card-content fxLayout="column" class="section-content">
|
||||
<div class="mat-body" *ngFor="let paragraph of advancedSettingsDesc">
|
||||
<p>{{paragraph}}</p>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
<mat-card-actions align="center">
|
||||
<button mat-button (click)="navigateToDocs()">VIEW DOCUMENTATION</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
|
@ -1,7 +1,7 @@
|
|||
@import "~@angular/material/theming";
|
||||
@import "../../../../../../../../node_modules/@angular/material/theming";
|
||||
@import "mycroft-colors";
|
||||
@import "components/buttons";
|
||||
@import "components/cards";
|
||||
@import "../../../../../../../../src/stylesheets/components/buttons";
|
||||
@import "../../../../../../../../src/stylesheets/components/cards";
|
||||
|
||||
mat-card {
|
||||
@include section-card;
|
||||
|
@ -31,10 +31,6 @@ mat-card {
|
|||
@include action-button-primary;
|
||||
margin-bottom: 16px;
|
||||
margin-right: 16px;
|
||||
|
||||
&:disabled {
|
||||
background-color: mat-color($mycroft-accent, 200);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { DeviceService } from '@account/http/device.service';
|
||||
import { Device } from '@account/models/device.model';
|
||||
import { Observable } from 'rxjs';
|
||||
import { switchMap, tap } from 'rxjs/operators';
|
||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
import { MatSnackBar, MatSnackBarConfig } from '@angular/material';
|
||||
|
||||
|
||||
const fiveSeconds = 5000;
|
||||
|
||||
@Component({
|
||||
selector: 'account-device-edit',
|
||||
templateUrl: './device-edit.component.html',
|
||||
styleUrls: ['./device-edit.component.scss']
|
||||
})
|
||||
export class DeviceEditComponent implements OnInit {
|
||||
public advancedSettingsDesc: string[];
|
||||
public deviceForm: FormGroup;
|
||||
private deviceId: string;
|
||||
public device$ = new Observable<Device>();
|
||||
private snackbarConfig = new MatSnackBarConfig();
|
||||
|
||||
constructor(
|
||||
private deviceService: DeviceService,
|
||||
private formBuilder: FormBuilder,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private snackbar: MatSnackBar
|
||||
) {
|
||||
this.snackbarConfig.panelClass = 'mycroft-no-action-snackbar';
|
||||
this.snackbarConfig.duration = fiveSeconds;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.buildAdvancedSettingsDesc();
|
||||
this.device$ = this.route.paramMap.pipe(
|
||||
switchMap((params: ParamMap) => this.deviceService.getDevice(params.get('deviceId'))),
|
||||
tap((device) => {
|
||||
this.deviceId = device.id;
|
||||
this.buildDeviceForm(device);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
buildDeviceForm(device) {
|
||||
this.deviceForm = this.formBuilder.group(
|
||||
{
|
||||
city: [device.city.name, Validators.required],
|
||||
name: [device.name, Validators.required],
|
||||
country: [device.country.name, Validators.required],
|
||||
placement: [device.placement],
|
||||
region: [device.region.name, Validators.required],
|
||||
timezone: [device.timezone.name, Validators.required],
|
||||
wakeWord: [device.wakeWord.displayName, Validators.required],
|
||||
voice: [device.voice.displayName, Validators.required]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
buildAdvancedSettingsDesc() {
|
||||
this.advancedSettingsDesc = [
|
||||
'Mycroft Core can be further configured ' +
|
||||
'for development and experimentation purposes. Example configurations ' +
|
||||
'include text-to-speech technologies, speech-to-text technologies and ' +
|
||||
'wake word listeners.',
|
||||
'These advanced options can be managed by editing a configuration file ' +
|
||||
'on the device. Proceed with caution; a bad configuration file could ' +
|
||||
'render your device unusable.',
|
||||
'Follow the link below for documentation on the options available ' +
|
||||
'and how to edit them.'
|
||||
];
|
||||
}
|
||||
|
||||
onExit(save: boolean) {
|
||||
if (save) {
|
||||
this.deviceService.updateDevice(this.deviceId, this.deviceForm).subscribe(
|
||||
() => {
|
||||
this.snackbar.open(
|
||||
'Device ' + this.deviceForm.controls['name'].value + ' updated' ,
|
||||
null,
|
||||
this.snackbarConfig
|
||||
);
|
||||
this.router.navigate(['/devices']);
|
||||
},
|
||||
() => {
|
||||
this.snackbar.open(
|
||||
'Error updating device',
|
||||
null,
|
||||
this.snackbarConfig
|
||||
);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.router.navigate(['/devices']);
|
||||
}
|
||||
}
|
||||
|
||||
navigateToDocs() {
|
||||
window.location.assign('https://mycroft.ai/documentation/mycroft-conf/');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<div fxLayout="row" fxLayoutAlign="center">
|
||||
<div
|
||||
id="add-device-button"
|
||||
fxFlex
|
||||
fxLayout="row"
|
||||
fxLayoutAlign="start center"
|
||||
routerLink="/devices/add"
|
||||
>
|
||||
<img src="../assets/generic-device-icon-blue.svg">
|
||||
<span fxFlex class="mat-h2">ADD DEVICE</span>
|
||||
<fa-icon class="mat-h2" [icon]="addIcon"></fa-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div fxLayout="row wrap" fxLayoutAlign="center" fxLayoutGap.gt-xs="16px">
|
||||
<mat-card *ngFor="let device of devices; let i = index">
|
||||
<mat-card-title fxLayout="row" fxLayoutAlign="start center">
|
||||
<img [src]="getDeviceIcon(device)"/>
|
||||
{{device.name}}
|
||||
</mat-card-title>
|
||||
<account-device-info [device]="device"></account-device-info>
|
||||
<mat-card-actions>
|
||||
<button mat-flat-button color="primary" [routerLink]="['./', device.id]">
|
||||
EDIT
|
||||
</button>
|
||||
<button mat-flat-button color="warn" (click)="onRemove(device, i)">
|
||||
REMOVE
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</div>
|
|
@ -1,12 +1,10 @@
|
|||
@import "~@angular/material/theming";
|
||||
@import '~src/stylesheets/mycroft-colors';
|
||||
@import '~src/stylesheets/components/buttons';
|
||||
@import 'mycroft-colors';
|
||||
@import 'components/buttons';
|
||||
|
||||
@mixin panel-defaults {
|
||||
border-radius: 12px;
|
||||
width: 330px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
|
@ -15,7 +13,8 @@
|
|||
@include panel-defaults;
|
||||
cursor: pointer;
|
||||
height: 50px;
|
||||
margin-top: 32px;
|
||||
max-width: 362px;
|
||||
min-width: 350px;
|
||||
|
||||
fa-icon {
|
||||
color: mat-color($mycroft-accent, 'A200');
|
||||
|
@ -44,7 +43,7 @@ mat-card {
|
|||
}
|
||||
|
||||
mat-card-title {
|
||||
color: mat-color($mycroft-primary, 700);
|
||||
color: mat-color($mycroft-primary);
|
||||
font-weight: bold;
|
||||
|
||||
img {
|
|
@ -1,12 +1,14 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material';
|
||||
import { MatDialog, MatSnackBar, MatSnackBarConfig } from '@angular/material';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { DeviceService } from '@account/http/device.service';
|
||||
import { Device } from '@account/models/device.model';
|
||||
import { RemoveComponent } from '../../../remove/remove.component';
|
||||
import { RemoveDeviceDialogComponent } from '../../components/modal/remove-device-dialog/remove-device-dialog.component';
|
||||
|
||||
const fiveSeconds = 5000;
|
||||
|
||||
@Component({
|
||||
selector: 'account-device-list',
|
||||
|
@ -17,19 +19,24 @@ export class DeviceListComponent implements OnInit {
|
|||
public addIcon = faPlus;
|
||||
public devices: Device[];
|
||||
public platforms = {
|
||||
'mark-one': {icon: '../assets/mark-1-icon.svg', displayName: 'Mark I'},
|
||||
'mark-two': {icon: '../assets/mark-2-icon.svg', displayName: 'Mark II'},
|
||||
'mycroft_mark_1': {icon: '../assets/mark-1-icon.svg', displayName: 'Mark I'},
|
||||
'mycroft_mark_2': {icon: '../assets/mark-2-icon.svg', displayName: 'Mark II'},
|
||||
'picroft': {icon: '../assets/picroft-icon.svg', displayName: 'Picroft'},
|
||||
'kde': {icon: '../assets/kde-icon.svg', displayName: 'KDE'}
|
||||
};
|
||||
private selectedDevice: Device;
|
||||
private snackbarConfig = new MatSnackBarConfig();
|
||||
|
||||
constructor(
|
||||
public dialog: MatDialog,
|
||||
private deviceService: DeviceService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
) { }
|
||||
private router: Router,
|
||||
private snackbar: MatSnackBar
|
||||
) {
|
||||
this.snackbarConfig.panelClass = 'mycroft-no-action-snackbar';
|
||||
this.snackbarConfig.duration = fiveSeconds;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe(
|
||||
|
@ -37,23 +44,34 @@ export class DeviceListComponent implements OnInit {
|
|||
);
|
||||
}
|
||||
|
||||
onRemovalClick(device: Device) {
|
||||
const removalDialogRef = this.dialog.open(RemoveComponent, {data: false});
|
||||
onRemove(device: Device, index: number) {
|
||||
const removalDialogRef = this.dialog.open(RemoveDeviceDialogComponent, {data: false});
|
||||
this.selectedDevice = device;
|
||||
removalDialogRef.afterClosed().subscribe(
|
||||
(result) => {
|
||||
if (result) { this.deviceService.deleteDevice(device); }
|
||||
if (result) { this.removeDevice(device, index); }
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onDeviceEdit(device: Device) {
|
||||
this.router.navigate(['/devices', device.id]);
|
||||
}
|
||||
|
||||
getPlatform(device: Device) {
|
||||
const knownPlatform = this.platforms[device.platform];
|
||||
return knownPlatform ? knownPlatform.displayName : device.platform;
|
||||
removeDevice(device: Device, index: number) {
|
||||
this.deviceService.deleteDevice(device).subscribe(
|
||||
() => {
|
||||
this.devices.splice(index, 1);
|
||||
this.snackbar.open(
|
||||
'Device removed successfully',
|
||||
null,
|
||||
this.snackbarConfig
|
||||
);
|
||||
},
|
||||
() => {
|
||||
this.snackbar.open(
|
||||
'An error occurred removing the device',
|
||||
null,
|
||||
this.snackbarConfig
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getDeviceIcon(device: Device) {
|
|
@ -1,27 +0,0 @@
|
|||
<div fxLayout="column" fxLayoutAlign="center center">
|
||||
<mat-tab-group>
|
||||
<mat-tab [label]="'Devices'">
|
||||
<account-device-list></account-device-list>
|
||||
</mat-tab>
|
||||
<mat-tab [label]="'Preferences'">
|
||||
<ng-template matTabContent>
|
||||
<account-device-preferences
|
||||
[deviceSetup]="false"
|
||||
[preferencesForm]="preferencesForm"
|
||||
[preferences]="preferences"
|
||||
>
|
||||
</account-device-preferences>
|
||||
</ng-template>
|
||||
</mat-tab>
|
||||
<mat-tab [label]="'Defaults'">
|
||||
<ng-template matTabContent>
|
||||
<account-device-edit
|
||||
[action]="'default edit'"
|
||||
[deviceForm]="defaultsForm"
|
||||
[defaults]="defaults"
|
||||
>
|
||||
</account-device-edit>
|
||||
</ng-template>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
|
@ -1,60 +0,0 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { AccountDefaults } from '@account/models/defaults.model';
|
||||
import { AccountPreferences } from '@account/models/preferences.model';
|
||||
|
||||
@Component({
|
||||
selector: 'account-devices',
|
||||
templateUrl: './devices.component.html',
|
||||
styleUrls: ['./devices.component.scss']
|
||||
})
|
||||
export class DevicesComponent implements OnInit {
|
||||
defaults: AccountDefaults;
|
||||
defaultsForm: FormGroup;
|
||||
preferences: AccountPreferences;
|
||||
preferencesForm: FormGroup;
|
||||
|
||||
constructor(private formBuilder: FormBuilder, private route: ActivatedRoute) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe(
|
||||
(data: {defaults: AccountDefaults, preferences: AccountPreferences}) => {
|
||||
this.defaults = data.defaults;
|
||||
this.preferences = data.preferences;
|
||||
}
|
||||
);
|
||||
|
||||
this.defaultsForm = this.formBuilder.group(
|
||||
{
|
||||
city: [this.defaults ? this.defaults.city.name : null],
|
||||
country: [this.defaults ? this.defaults.country.name : null],
|
||||
name: [null],
|
||||
pairingCode: [null],
|
||||
placement: [null],
|
||||
region: [this.defaults ? this.defaults.region.name : null],
|
||||
timezone: [this.defaults ? this.defaults.timezone.name : null],
|
||||
wakeWord: [this.defaults ? this.defaults.wakeWord.displayName : null],
|
||||
voice: [this.defaults ? this.defaults.voice.displayName : null]
|
||||
}
|
||||
);
|
||||
this.preferencesForm = this.formBuilder.group(
|
||||
{
|
||||
dateFormat: [
|
||||
this.preferences ? this.preferences.dateFormat : null,
|
||||
Validators.required
|
||||
],
|
||||
measurementSystem: [
|
||||
this.preferences ? this.preferences.measurementSystem : null,
|
||||
Validators.required
|
||||
],
|
||||
timeFormat: [
|
||||
this.preferences ? this.preferences.timeFormat : null,
|
||||
Validators.required
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<account-preferences-card [preferencesForm]="preferencesForm">
|
||||
<mat-card-title>Manage your device preferences</mat-card-title>
|
||||
<button mat-button (click)="onSave()" [disabled]="!preferencesForm.valid">SAVE</button>
|
||||
</account-preferences-card>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
@import "~@angular/material/theming";
|
||||
@import "mycroft-colors";
|
||||
@import "components/buttons";
|
||||
@import "components/cards";
|
||||
|
||||
mat-card-title {
|
||||
color: mat-color($mycroft-primary)
|
||||
}
|
||||
|
||||
button {
|
||||
@include action-button-primary;
|
||||
margin: 16px;
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
|
||||
import { AccountPreferences } from '@account/models/preferences.model';
|
||||
import { DeviceService } from '../../../../../core/http/device.service';
|
||||
import { DeviceService } from '@account/http/device.service';
|
||||
import { OptionButtonsConfig } from '@account/models/option-buttons-config.model';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'account-device-preferences',
|
||||
|
@ -11,15 +12,14 @@ import { OptionButtonsConfig } from '@account/models/option-buttons-config.model
|
|||
styleUrls: ['./preferences.component.scss']
|
||||
})
|
||||
export class PreferencesComponent implements OnInit {
|
||||
public advancedSettingsDesc: string[];
|
||||
@Input() deviceSetup: boolean;
|
||||
@Input() preferences: AccountPreferences;
|
||||
@Input() preferencesForm: FormGroup;
|
||||
public preferences: AccountPreferences;
|
||||
public preferencesForm: FormGroup;
|
||||
public measurementOptionsConfig: OptionButtonsConfig;
|
||||
public timeFormatOptionsConfig: OptionButtonsConfig;
|
||||
public dateFormatOptionsConfig: OptionButtonsConfig;
|
||||
|
||||
constructor(private deviceService: DeviceService) {
|
||||
constructor(private deviceService: DeviceService, private formBuilder: FormBuilder, private route: ActivatedRoute) {
|
||||
this.dateFormatOptionsConfig = {
|
||||
label: 'Date Format',
|
||||
options: ['DD/MM/YYYY', 'MM/DD/YYYY'],
|
||||
|
@ -41,33 +41,29 @@ export class PreferencesComponent implements OnInit {
|
|||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.buildAdvancedSettingsDesc();
|
||||
this.route.data.subscribe(
|
||||
(data: {preferences: AccountPreferences}) => { this.preferences = data.preferences; }
|
||||
);
|
||||
this.buildForm();
|
||||
}
|
||||
|
||||
buildAdvancedSettingsDesc() {
|
||||
this.advancedSettingsDesc = [
|
||||
'Mycroft Core can be further configured ' +
|
||||
'for development and experimentation purposes. Example configurations ' +
|
||||
'include text-to-speech technologies, speech-to-text technologies and ' +
|
||||
'wake word listeners.',
|
||||
'These advanced options can be managed by editing a configuration file ' +
|
||||
'on the device. Proceed with caution; a bad configuration file could ' +
|
||||
'render your device unusable.',
|
||||
'Follow the link below for documentation on the options available ' +
|
||||
'and how to edit them.'
|
||||
];
|
||||
}
|
||||
|
||||
changeDateFormat(newValue: string) {
|
||||
this.preferencesForm.patchValue({dateFormat: newValue});
|
||||
}
|
||||
|
||||
changeMeasurementSystem(newValue: string) {
|
||||
this.preferencesForm.patchValue({measurementSystem: newValue});
|
||||
}
|
||||
|
||||
changeTimeFormat(newValue: string) {
|
||||
this.preferencesForm.patchValue({timeFormat: newValue});
|
||||
buildForm() {
|
||||
this.preferencesForm = this.formBuilder.group(
|
||||
{
|
||||
dateFormat: [
|
||||
this.preferences ? this.preferences.dateFormat : null,
|
||||
Validators.required
|
||||
],
|
||||
measurementSystem: [
|
||||
this.preferences ? this.preferences.measurementSystem : null,
|
||||
Validators.required
|
||||
],
|
||||
timeFormat: [
|
||||
this.preferences ? this.preferences.timeFormat : null,
|
||||
Validators.required
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onSave() {
|
|
@ -1,12 +0,0 @@
|
|||
<div mat-dialog-title class="mat-h2-primary">Remove Device</div>
|
||||
|
||||
<div mat-dialog-content>
|
||||
<div class="mat-body">
|
||||
Just double checking. Device removal cannot be undone.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div mat-dialog-actions align="end">
|
||||
<button id="device-remove-cancel-button" mat-button (click)="onCancelClick()">CANCEL</button>
|
||||
<button id="device-remove-button" mat-button [mat-dialog-close]="true">REMOVE</button>
|
||||
</div>
|
|
@ -4,18 +4,33 @@
|
|||
</mat-toolbar>
|
||||
<mat-card-content fxLayout="column" fxLayoutAlign="start none">
|
||||
<ng-container *ngFor="let agreement of account.agreements">
|
||||
<div *ngIf="agreement.type !== 'Open Dataset'" class="profile-text" fxLayout="row" fxLayout.lt-md="column" fxLayoutAlign="start center">
|
||||
<span class="mat-subheading-2">{{agreement.type}}:</span>
|
||||
<p class="mat-body">Accepted {{agreement.acceptDate}}</p>
|
||||
<div
|
||||
*ngIf="agreement.type !== 'Open Dataset'"
|
||||
class="profile-text"
|
||||
fxLayout="row"
|
||||
fxLayout.lt-md="column"
|
||||
fxLayoutAlign="space-between center"
|
||||
>
|
||||
<span fxFlex.gt-sm="30" class="mat-subheading-2">{{agreement.type}}:</span>
|
||||
<p fxFlex class="mat-body">Accepted {{agreement.acceptDate}}</p>
|
||||
<a fxFlex.gt-sm="20" mat-button target="_blank" href="{{buildAgreementUrl(agreement.type)}}">
|
||||
<fa-icon [icon]="documentIcon"></fa-icon>
|
||||
View Document
|
||||
</a>
|
||||
</div>
|
||||
</ng-container>
|
||||
<!--<div fxLayout="row" fxLayoutAlign="start center">-->
|
||||
<!--<span class="mat-subheading-2">Username:</span>-->
|
||||
<!--<p class="mat-body">{{agreement.username}}</p>-->
|
||||
<!--</div>-->
|
||||
<!--<div fxLayout="column" fxLayoutAlign="center">-->
|
||||
<!--<button mat-button>CHANGE USERNAME</button>-->
|
||||
<!--<button mat-button>CHANGE PASSWORD</button>-->
|
||||
<!--</div>-->
|
||||
<div
|
||||
class="profile-text"
|
||||
fxLayout="row"
|
||||
fxLayout.lt-md="column"
|
||||
fxLayoutAlign="space-between center"
|
||||
>
|
||||
<span fxFlex.gt-sm="30" class="mat-subheading-2">Open Dataset:</span>
|
||||
<p fxFlex class="mat-body">{{openDatasetOptInDate}}</p>
|
||||
<button fxFlex.gt-sm="20" mat-button (click)="onOptInOrOut()">
|
||||
<fa-icon [icon]="optInOutIcon"></fa-icon>
|
||||
{{openDatasetButtonText}}
|
||||
</button>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
|
|
@ -1,26 +1,35 @@
|
|||
@import "../../../../../../../../../src/stylesheets/components/cards";
|
||||
@import "~@angular/material/theming";
|
||||
@import "components/cards";
|
||||
@import "mycroft-colors";
|
||||
|
||||
mat-card {
|
||||
@include section-card;
|
||||
|
||||
mat-card-content {
|
||||
margin-left: 16px;
|
||||
margin-right: 8px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 700px;
|
||||
|
||||
.mat-subheading-2 {
|
||||
color: mat-color($mycroft-accent, A700);
|
||||
font-weight: bold;
|
||||
font-weight: bolder;
|
||||
margin: 8px;
|
||||
min-width: 120px;
|
||||
}
|
||||
.mat-body {
|
||||
margin: 0;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
// button {
|
||||
// @include action-button-primary;
|
||||
// margin: 8px;
|
||||
// }
|
||||
button {
|
||||
color: mat-color($mycroft-primary);
|
||||
}
|
||||
|
||||
a {
|
||||
color: mat-color($mycroft-primary);
|
||||
}
|
||||
|
||||
fa-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
|
||||
import { Account } from '../../../../../shared/models/account.model';
|
||||
import { faFileAlt, faSignInAlt, faSignOutAlt, IconDefinition } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { Account } from '@account/models/account.model';
|
||||
|
||||
@Component({
|
||||
selector: 'account-agreements-edit',
|
||||
|
@ -9,10 +11,52 @@ import { Account } from '../../../../../shared/models/account.model';
|
|||
})
|
||||
export class AgreementsComponent implements OnInit {
|
||||
@Input() account: Account;
|
||||
@Output() openDatasetOptIn = new EventEmitter<boolean>();
|
||||
public documentIcon = faFileAlt;
|
||||
public optInOutIcon: IconDefinition;
|
||||
public openDatasetOptInDate: string;
|
||||
public openDatasetButtonText: string;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
const openDatasetAgreement = this.account.agreements.find(
|
||||
(agreement) => agreement.type === 'Open Dataset'
|
||||
);
|
||||
|
||||
if (openDatasetAgreement) {
|
||||
this.optInOutIcon = faSignOutAlt;
|
||||
this.openDatasetButtonText = 'Opt Out';
|
||||
this.openDatasetOptInDate = 'Opted in ' + openDatasetAgreement.acceptDate;
|
||||
} else {
|
||||
this.optInOutIcon = faSignInAlt;
|
||||
this.openDatasetButtonText = 'Opt In';
|
||||
this.openDatasetOptInDate = 'Opted out';
|
||||
}
|
||||
}
|
||||
|
||||
buildAgreementUrl(agreementType: string): string {
|
||||
let url = 'https://mycroft.ai/';
|
||||
if (agreementType === 'Privacy Policy') {
|
||||
url += 'embed-privacy-policy';
|
||||
} else {
|
||||
url += 'embed-terms-of-use';
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
onOptInOrOut() {
|
||||
this.openDatasetOptIn.emit(this.openDatasetButtonText === 'Opt In');
|
||||
if (this.openDatasetButtonText === 'Opt In') {
|
||||
this.optInOutIcon = faSignOutAlt;
|
||||
this.openDatasetButtonText = 'Opt Out';
|
||||
this.openDatasetOptInDate = 'Opted in just now';
|
||||
} else {
|
||||
this.optInOutIcon = faSignInAlt;
|
||||
this.openDatasetButtonText = 'Opt In';
|
||||
this.openDatasetOptInDate = 'Opted out';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<account-membership-options
|
||||
[accountMembership]="accountMembership"
|
||||
[membershipTypes]="membershipTypes"
|
||||
(membershipChange)="onMembershipChange($event)"
|
||||
(membershipChange)="updateAccount($event)"
|
||||
>
|
||||
</account-membership-options>
|
||||
<span *ngIf="accountMembership && accountMembership.duration" id="subscription-date" class="mat-body">
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import { Component, Input, OnDestroy } from '@angular/core';
|
||||
import { MediaChange, MediaObserver } from '@angular/flex-layout';
|
||||
import { MatBottomSheet } from '@angular/material';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { AccountMembership } from '@account/models/account-membership.model';
|
||||
import { MembershipType } from '@account/models/membership.model';
|
||||
import { MembershipUpdate } from '@account/models/membership-update.model';
|
||||
import { ProfileService } from '@account/http/profile.service';
|
||||
import { PaymentComponent } from '../../views/payment/payment.component';
|
||||
|
||||
const twoSeconds = 2000;
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'account-membership-edit',
|
||||
|
@ -15,14 +18,14 @@ import { PaymentComponent } from '../../views/payment/payment.component';
|
|||
})
|
||||
export class MembershipComponent implements OnDestroy {
|
||||
@Input() accountMembership: AccountMembership;
|
||||
public alignVertical: boolean;
|
||||
@Input() membershipTypes: MembershipType[];
|
||||
public alignVertical: boolean;
|
||||
private mediaWatcher: Subscription;
|
||||
|
||||
constructor(
|
||||
public bottomSheet: MatBottomSheet,
|
||||
public mediaObserver: MediaObserver,
|
||||
private profileService: ProfileService,
|
||||
private snackbar: MatSnackBar
|
||||
) {
|
||||
this.mediaWatcher = mediaObserver.media$.subscribe(
|
||||
(change: MediaChange) => {
|
||||
|
@ -35,43 +38,15 @@ export class MembershipComponent implements OnDestroy {
|
|||
this.mediaWatcher.unsubscribe();
|
||||
}
|
||||
|
||||
onMembershipChange(membershipType: string) {
|
||||
const selectedMembership = this.membershipTypes.find(
|
||||
(membership) => membership.type === membershipType
|
||||
);
|
||||
if (selectedMembership) {
|
||||
if (this.accountMembership) {
|
||||
// We have the user's credit card info but they decide to change plans
|
||||
this.profileService.updateAccount(
|
||||
{membership: {paymentMethod: 'Stripe', newMembership: false, membershipType: membershipType}}
|
||||
);
|
||||
} else {
|
||||
// No credit card info. Go to payment screen to collect
|
||||
this.openBottomSheet(membershipType);
|
||||
}
|
||||
} else {
|
||||
// Membership termination
|
||||
this.profileService.updateAccount(
|
||||
{membership: {paymentMethod: 'Stripe', newMembership: false, membershipType: null}}
|
||||
updateAccount(membershipUpdate: MembershipUpdate) {
|
||||
const accountUpdate = {membership: membershipUpdate};
|
||||
this.profileService.updateAccount(accountUpdate).subscribe(
|
||||
() => {
|
||||
this.snackbar.open(
|
||||
'Membership updated',
|
||||
null,
|
||||
{panelClass: 'mycroft-no-action-snackbar', duration: twoSeconds}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
openBottomSheet(membershipType: string) {
|
||||
const bottomSheetConfig = {
|
||||
data: {newAccount: false, membershipType: membershipType},
|
||||
disableClose: true,
|
||||
restoreFocus: true
|
||||
};
|
||||
const bottomSheetRef = this.bottomSheet.open(PaymentComponent, bottomSheetConfig);
|
||||
bottomSheetRef.afterDismissed().subscribe(
|
||||
(dismissValue) => {
|
||||
if (dismissValue === 'cancel') {
|
||||
this.profileService.setSelectedMembershipType(
|
||||
this.accountMembership,
|
||||
this.membershipTypes
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<mat-button-toggle-group
|
||||
class="options-button-group"
|
||||
[vertical]="alignVertical"
|
||||
[value]="selectedMembershipType"
|
||||
[(ngModel)]="selectedMembershipType"
|
||||
(change)="onMembershipSelect($event)"
|
||||
>
|
||||
<mat-button-toggle
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { MediaChange, MediaObserver } from '@angular/flex-layout';
|
||||
import { MatButtonToggleChange } from '@angular/material';
|
||||
import { MatButtonToggleChange, MatDialog, MatDialogConfig, MatSnackBar } from '@angular/material';
|
||||
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { AccountMembership } from '../../../../../shared/models/account-membership.model';
|
||||
import { MembershipType } from '../../../../../shared/models/membership.model';
|
||||
import { ProfileService } from '../../../../../core/http/profile.service';
|
||||
import { AccountMembership } from '@account/models/account-membership.model';
|
||||
import { MembershipType } from '@account/models/membership.model';
|
||||
import { ProfileService } from '@account/http/profile.service';
|
||||
import { PaymentComponent } from '@account/app/modules/profile/components/views/payment/payment.component';
|
||||
import { MembershipUpdate } from '@account/models/membership-update.model';
|
||||
|
||||
|
||||
@Component({
|
||||
|
@ -16,13 +18,16 @@ import { ProfileService } from '../../../../../core/http/profile.service';
|
|||
})
|
||||
export class MembershipOptionsComponent implements OnInit, OnDestroy {
|
||||
@Input() accountMembership: AccountMembership;
|
||||
public alignVertical: boolean;
|
||||
@Input() membershipTypes: MembershipType[];
|
||||
@Output() membershipChange = new EventEmitter<MembershipUpdate>();
|
||||
public alignVertical: boolean;
|
||||
public mediaWatcher: Subscription;
|
||||
@Output() membershipChange = new EventEmitter<string>();
|
||||
public selectedMembershipType: string;
|
||||
|
||||
constructor(public mediaObserver: MediaObserver, private profileService: ProfileService) {
|
||||
constructor(
|
||||
public mediaObserver: MediaObserver,
|
||||
public paymentDialog: MatDialog,
|
||||
) {
|
||||
this.mediaWatcher = mediaObserver.media$.subscribe(
|
||||
(change: MediaChange) => {
|
||||
this.alignVertical = ['xs', 'sm'].includes(change.mqAlias);
|
||||
|
@ -31,23 +36,70 @@ export class MembershipOptionsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.profileService.selectedMembershipType.subscribe(
|
||||
(membershipType) => { this.selectedMembershipType = membershipType; }
|
||||
);
|
||||
this.profileService.setSelectedMembershipType(
|
||||
this.accountMembership,
|
||||
this.membershipTypes
|
||||
);
|
||||
|
||||
this.setSelectedMembershipType();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.mediaWatcher.unsubscribe();
|
||||
}
|
||||
|
||||
onMembershipSelect(newMembershipType: MatButtonToggleChange) {
|
||||
this.profileService.selectedMembershipType.next(newMembershipType.value);
|
||||
this.membershipChange.emit(newMembershipType.value);
|
||||
setSelectedMembershipType() {
|
||||
let selectedMembership: MembershipType;
|
||||
if (this.accountMembership) {
|
||||
selectedMembership = this.membershipTypes.find(
|
||||
(membershipType) => membershipType.type === this.accountMembership.type
|
||||
);
|
||||
this.selectedMembershipType = selectedMembership.type;
|
||||
} else {
|
||||
this.selectedMembershipType = 'Maybe Later';
|
||||
}
|
||||
}
|
||||
|
||||
onMembershipSelect(membershipType: MatButtonToggleChange) {
|
||||
const selectedMembership = this.membershipTypes.find(
|
||||
(membership) => membership.type === membershipType.value
|
||||
);
|
||||
let membershipUpdate;
|
||||
if (selectedMembership) {
|
||||
if (this.accountMembership) {
|
||||
// We have the user's credit card info but they decide to change plans
|
||||
membershipUpdate = {
|
||||
paymentMethod: 'Stripe',
|
||||
newMembership: false,
|
||||
membershipType: membershipType
|
||||
};
|
||||
this.membershipChange.emit(membershipUpdate);
|
||||
} else {
|
||||
// No credit card info. Go to payment dialog to collect
|
||||
this.openPaymentDialog(membershipType.value);
|
||||
}
|
||||
} else {
|
||||
// Membership termination
|
||||
membershipUpdate = {newMembership: false, membershipType: null};
|
||||
this.membershipChange.emit(membershipUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
openPaymentDialog(membershipType: string) {
|
||||
const dialogConfig = new MatDialogConfig();
|
||||
dialogConfig.data = {newAccount: false, membershipType: membershipType};
|
||||
dialogConfig.disableClose = true;
|
||||
dialogConfig.restoreFocus = true;
|
||||
const dialogRef = this.paymentDialog.open(PaymentComponent, dialogConfig);
|
||||
dialogRef.afterClosed().subscribe(
|
||||
(stripeToken) => {
|
||||
if (stripeToken) {
|
||||
const membershipUpdate: MembershipUpdate = {
|
||||
newMembership: true,
|
||||
membershipType: membershipType,
|
||||
paymentMethod: 'Stripe',
|
||||
paymentToken: stripeToken
|
||||
};
|
||||
this.membershipChange.emit(membershipUpdate);
|
||||
} else {
|
||||
this.setSelectedMembershipType();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,5 +17,5 @@
|
|||
</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button id="cancel-button" mat-button (click)="onCancel()">CANCEL</button>
|
||||
<button id="confirm-button" mat-button (click)="onConfirm()">CONFIRM</button>
|
||||
<button id="confirm-button" mat-button (click)="onConfirm()">DELETE</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
<mat-card fxLayout.gt-sm="row" fxLayoutAlign="center center" class="mat-elevation-z0" [formGroup]="newAcctForm.get('login')">
|
||||
<!-- Federated Log In Controls -->
|
||||
<mat-card class="mat-elevation-z0">
|
||||
<h2 class="mat-h2">Log In Using...</h2>
|
||||
<p>{{federatedLoginText}}</p>
|
||||
<div id="federated-buttons" fxLayout="column" fxLayoutAlign="center center">
|
||||
<shared-google-button></shared-google-button>
|
||||
<shared-facebook-button (facebookEmail)="onFacebookLogin($event)"></shared-facebook-button>
|
||||
<shared-github-button></shared-github-button>
|
||||
</div>
|
||||
</mat-card>
|
||||
|
||||
<h1 class="mat-h1">OR</h1>
|
||||
|
||||
<!-- Mycroft Log In Controls-->
|
||||
<mat-card class="mat-elevation-z0">
|
||||
<h2 class="mat-h2">Email and Password</h2>
|
||||
<p>{{internalLoginText}}</p>
|
||||
<div fxLayout="column">
|
||||
<mat-form-field [appearance]="'outline'">
|
||||
<mat-label>Email Address</mat-label>
|
||||
<input matInput type="email" formControlName="userEnteredEmail" [readonly]="disableInternal">
|
||||
<mat-error>
|
||||
Must be a valid email address
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field [appearance]="'outline'">
|
||||
<mat-label>Password</mat-label>
|
||||
<input matInput type="password" formControlName="password" [readonly]="disableInternal">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</mat-card>
|
||||
</mat-card>
|
|
@ -1,31 +0,0 @@
|
|||
@import "../../../../../../../../../node_modules/@angular/material/theming";
|
||||
@import "mycroft-colors";
|
||||
|
||||
.mat-h1 {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.mat-h2 {
|
||||
color: mat-color($mycroft-primary);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
mat-card {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 1000px;
|
||||
|
||||
mat-card {
|
||||
height: 300px;
|
||||
max-width: 400px;
|
||||
|
||||
#federated-buttons {
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
max-width: 400px;
|
||||
min-width: 300px;
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'account-authentication-step',
|
||||
templateUrl: './authentication-step.component.html',
|
||||
styleUrls: ['./authentication-step.component.scss']
|
||||
})
|
||||
export class AuthenticationStepComponent implements OnInit {
|
||||
public disableInternal = false;
|
||||
public federatedLoginText: string;
|
||||
public internalLoginText: string;
|
||||
@Input() newAcctForm: FormGroup;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
this.federatedLoginText = 'To use this option, you must allow the ' +
|
||||
'provider to share your email address with Mycroft';
|
||||
this.internalLoginText = 'Login credentials stored on Mycroft ' +
|
||||
'servers are encrypted for your privacy and protection.';
|
||||
}
|
||||
|
||||
onFacebookLogin(email: string) {
|
||||
this.newAcctForm.patchValue({login: {federatedEmail: email}});
|
||||
this.disableInternal = true;
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
<mat-card fxLayout="column" fxLayoutAlign="center center" class="mat-elevation-z0">
|
||||
<h1 class="mat-h1">You're Done!</h1>
|
||||
<p class="mat-body">Your account is ready to be created. We are excited to have you as part of our community.</p>
|
||||
<button mat-button>Create Account and Login</button>
|
||||
</mat-card>
|
|
@ -1,17 +0,0 @@
|
|||
@import "../../../../../../../../../node_modules/@angular/material/theming";
|
||||
@import "../../../../../../../../../src/stylesheets/components/buttons";
|
||||
|
||||
mat-card {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 700px;
|
||||
|
||||
h1 {
|
||||
color: mat-color($mycroft-primary);
|
||||
}
|
||||
|
||||
button {
|
||||
@include action-button-primary;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'account-done-step',
|
||||
templateUrl: './done-step.component.html',
|
||||
styleUrls: ['./done-step.component.scss']
|
||||
})
|
||||
export class DoneStepComponent implements OnInit {
|
||||
@Input() newAcctForm: FormGroup;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<img src="../assets/membership.svg">
|
||||
<mat-card class="mat-elevation-z0">
|
||||
<mat-card-title>Become a Member</mat-card-title>
|
||||
<mat-card-content>
|
||||
<p *ngFor="let paragraph of membershipDescription" class="mat-body">
|
||||
{{paragraph}}
|
||||
</p>
|
||||
<account-membership-options
|
||||
[membershipTypes]="membershipTypes"
|
||||
(membershipChange)="updateNewAccountForm($event)"
|
||||
>
|
||||
</account-membership-options>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
@import "~@angular/material/theming";
|
||||
@import "mycroft-colors";
|
||||
@import "components/buttons";
|
||||
|
||||
img {
|
||||
padding-left: 16px;
|
||||
}
|
||||
mat-card {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 1000px;
|
||||
|
||||
mat-card-title {
|
||||
color: mat-color($mycroft-primary);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
mat-button-toggle-group {
|
||||
@include options-button-group
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { MembershipType } from '@account/models/membership.model';
|
||||
import { MembershipUpdate } from '@account/models/membership-update.model';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'account-membership-step',
|
||||
templateUrl: './membership-step.component.html',
|
||||
styleUrls: ['./membership-step.component.scss']
|
||||
})
|
||||
export class MembershipStepComponent implements OnInit {
|
||||
@Input() membershipTypes: MembershipType[];
|
||||
@Input() newAcctForm: FormGroup;
|
||||
public membershipDescription: string[];
|
||||
|
||||
constructor() {
|
||||
this.membershipDescription = [
|
||||
'Mycroft\'s voice assistant software is open source, which means it is free to use and ' +
|
||||
'the underlying source code is available to the public. Our entire platform is free ' +
|
||||
'of advertisements. The data we collect from those that opt in to our open dataset ' +
|
||||
'is not sold to anyone.',
|
||||
'While many contributions to the Mycroft platform come in the form ' +
|
||||
'of volunteer work by community members, there is also a small team employed by Mycroft AI. ' +
|
||||
'The team curates the software, supports the community and ensures the privacy of your data. ' +
|
||||
'Your donation will help ensure our team can continue providing these services to our ' +
|
||||
'users and community.',
|
||||
'Members will receive benefits like access to premium voices.'
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
updateNewAccountForm(membershipUpdate: MembershipUpdate): void {
|
||||
this.newAcctForm.patchValue(
|
||||
{
|
||||
membership: {
|
||||
newMembership: membershipUpdate.newMembership,
|
||||
membershipType: membershipUpdate.membershipType,
|
||||
paymentMethod: membershipUpdate.paymentMethod,
|
||||
paymentToken: membershipUpdate.paymentToken
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<mat-card class="mat-elevation-z0">
|
||||
<mat-card-title>Join Mycroft's Open Dataset</mat-card-title>
|
||||
<mat-card-content>
|
||||
<p *ngFor="let paragraph of openDatasetDescription" class="mat-body">
|
||||
{{paragraph}}
|
||||
</p>
|
||||
<mat-button-toggle-group>
|
||||
<mat-button-toggle (click)="onOptIn()">OPT IN</mat-button-toggle>
|
||||
<mat-button-toggle (click)="onOptOut()">MAYBE LATER</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
|
@ -0,0 +1,18 @@
|
|||
@import "~@angular/material/theming";
|
||||
@import "mycroft-colors";
|
||||
@import "components/buttons";
|
||||
|
||||
mat-card {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 1000px;
|
||||
|
||||
mat-card-title {
|
||||
color: mat-color($mycroft-primary);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
mat-button-toggle-group {
|
||||
@include options-button-group
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'account-open-dataset-step',
|
||||
templateUrl: './open-dataset-step.component.html',
|
||||
styleUrls: ['./open-dataset-step.component.scss']
|
||||
})
|
||||
export class OpenDatasetStepComponent implements OnInit {
|
||||
@Input() newAcctForm: FormGroup;
|
||||
public openDatasetDescription: string[];
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.openDatasetDescription = [
|
||||
'Mycroft\'s voices and services can only improve with your help. ' +
|
||||
'By joining our open dataset, you agree to allow Mycroft AI to collect data related ' +
|
||||
'to your interactions with devices running Mycroft\'s voice assistant software. ' +
|
||||
'We pledge to use this contribution in a responsible way.',
|
||||
'Your data will also be made available to other researchers in the ' +
|
||||
'voice AI space with values that align with our own, like Mozilla Common Voice. ' +
|
||||
'As part of their agreement with Mycroft AI to access this data, they will be ' +
|
||||
'required to honor your request to remove any trace of your contributions if you ' +
|
||||
'decide to opt out.',
|
||||
'You can opt in or out of the open dataset at any time on your account profile page.',
|
||||
'We thank you in advance for helping to improve Mycroft\'s services!'
|
||||
];
|
||||
}
|
||||
|
||||
onOptIn(): void {
|
||||
this.newAcctForm.patchValue({openDataset: true});
|
||||
}
|
||||
|
||||
onOptOut(): void {
|
||||
this.newAcctForm.patchValue({openDataset: false});
|
||||
}
|
||||
}
|
|
@ -7,8 +7,8 @@
|
|||
<ngx-stripe-card [options]="cardOptions"></ngx-stripe-card>
|
||||
</mat-card>
|
||||
</mat-card-content>
|
||||
<mat-card-actions align="right">
|
||||
<button mat-button id="cancel-button" (click)="onCancel()">CANCEL</button>
|
||||
<button mat-button id="submit-button" (click)="submitPayment()">SUBMIT</button>
|
||||
<mat-card-actions [align]="'end'">
|
||||
<button mat-button (click)="onCancel()">CANCEL</button>
|
||||
<button mat-button (click)="submitPaymentInfo()" class="submit-button">SUBMIT</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
@import "../../../../../../../../../node_modules/@angular/material/theming";
|
||||
@import "~@angular/material/theming";
|
||||
@import "mycroft-colors";
|
||||
@import "../../../../../../../../../src/stylesheets/components/buttons";
|
||||
@import "components/buttons";
|
||||
|
||||
mat-card {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
margin-bottom: 16px;
|
||||
max-width: 500px;
|
||||
padding: 32px;
|
||||
padding: 0;
|
||||
|
||||
mat-card-title {
|
||||
color: mat-color($mycroft-primary, 700);
|
||||
|
@ -28,12 +27,18 @@ mat-card {
|
|||
border-width: 1px;
|
||||
border-color: mat-color($mycroft-accent, 200);
|
||||
padding: 16px;
|
||||
margin-bottom: 40px;
|
||||
width: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
#submit-button {
|
||||
@include action-button-primary;
|
||||
margin-top: 16px
|
||||
.mat-card-actions.mat-card-actions {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
.submit-button {
|
||||
@include action-button-primary;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { Component, Inject, OnInit, ViewChild } from '@angular/core';
|
||||
import {
|
||||
MatBottomSheetRef,
|
||||
MatDialog,
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogRef,
|
||||
MatSnackBar
|
||||
} from '@angular/material';
|
||||
|
@ -9,7 +8,6 @@ import {
|
|||
import { ElementOptions, StripeCardComponent, StripeService } from 'ngx-stripe';
|
||||
|
||||
import { ProfileService } from '@account/http/profile.service';
|
||||
import { VerifyCardDialogComponent } from './verify-card-dialog.component';
|
||||
|
||||
const twoSeconds = 2000;
|
||||
|
||||
|
@ -32,14 +30,13 @@ export class PaymentComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
};
|
||||
private dialogRef: MatDialogRef<VerifyCardDialogComponent>;
|
||||
|
||||
constructor(
|
||||
private bottomSheetRef: MatBottomSheetRef<PaymentComponent>,
|
||||
private paymentSnackbar: MatSnackBar,
|
||||
public dialogRef: MatDialogRef<PaymentComponent>,
|
||||
private snackbar: MatSnackBar,
|
||||
private profileService: ProfileService,
|
||||
private stripeService: StripeService,
|
||||
public verifyCardDialog: MatDialog
|
||||
@Inject(MAT_DIALOG_DATA) public dialogData: any
|
||||
|
||||
) {
|
||||
}
|
||||
|
@ -47,60 +44,23 @@ export class PaymentComponent implements OnInit {
|
|||
ngOnInit() {
|
||||
}
|
||||
|
||||
submitPayment() {
|
||||
this.openDialog();
|
||||
submitPaymentInfo() {
|
||||
this.stripeService.createToken(this.card.getCard(), {}).subscribe(
|
||||
result => {
|
||||
if (result.token) {
|
||||
const configData = this.bottomSheetRef.containerInstance.bottomSheetConfig.data;
|
||||
if (configData.newAccount) {
|
||||
this.showStripeSuccess(result.token.id);
|
||||
} else {
|
||||
this.updateAccount(configData.membershipType, result.token.id);
|
||||
}
|
||||
this.dialogRef.close(result.token.id);
|
||||
} else if (result.error) {
|
||||
this.showStripeError(result.error.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
(result) => { this.showStripeError(result.toString()); }
|
||||
);
|
||||
}
|
||||
|
||||
openDialog(): void {
|
||||
this.dialogRef = this.verifyCardDialog.open(
|
||||
VerifyCardDialogComponent,
|
||||
{width: '250px'}
|
||||
);
|
||||
}
|
||||
|
||||
updateAccount(membershipType: string, stripeToken: string) {
|
||||
const newMembership = {
|
||||
membership: {
|
||||
newMembership: true,
|
||||
membershipType: membershipType,
|
||||
paymentMethod: 'Stripe',
|
||||
paymentToken: stripeToken
|
||||
}
|
||||
};
|
||||
this.profileService.updateAccount(newMembership).subscribe(
|
||||
() => { this.showStripeSuccess(stripeToken); }
|
||||
);
|
||||
}
|
||||
|
||||
showStripeSuccess(stripeToken: string) {
|
||||
this.dialogRef.close();
|
||||
const paymentSnackbarRef = this.paymentSnackbar.open(
|
||||
'Card verification successful',
|
||||
null,
|
||||
{panelClass: 'mycroft-no-action-snackbar', duration: twoSeconds}
|
||||
);
|
||||
paymentSnackbarRef.afterDismissed().subscribe(
|
||||
() => { this.bottomSheetRef.dismiss(stripeToken); }
|
||||
);
|
||||
}
|
||||
|
||||
showStripeError(errorMessage: string) {
|
||||
this.dialogRef.close();
|
||||
this.paymentSnackbar.open(
|
||||
this.snackbar.open(
|
||||
errorMessage,
|
||||
null,
|
||||
{panelClass: 'mycroft-no-action-snackbar', duration: twoSeconds}
|
||||
|
@ -108,6 +68,6 @@ export class PaymentComponent implements OnInit {
|
|||
}
|
||||
|
||||
onCancel() {
|
||||
this.bottomSheetRef.dismiss('cancel');
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
<mat-card class="mat-elevation-z0">
|
||||
<h2 class="mat-h2">Become a Member</h2>
|
||||
<p *ngFor="let paragraph of membershipDescription" class="mat-body">
|
||||
{{paragraph}}
|
||||
</p>
|
||||
<account-membership-options
|
||||
[membershipTypes]="membershipTypes"
|
||||
(membershipChange)="onMembershipSelection($event)"
|
||||
>
|
||||
</account-membership-options>
|
||||
</mat-card>
|
||||
|
||||
<mat-card class="mat-elevation-z0">
|
||||
<h2 class="mat-h2">Join Mycroft's Open Dataset</h2>
|
||||
<p *ngFor="let paragraph of openDatasetDescription" class="mat-body">
|
||||
{{paragraph}}
|
||||
</p>
|
||||
<mat-button-toggle-group>
|
||||
<mat-button-toggle (click)="onOptIn()">Opt Into the Mycroft Open Dataset</mat-button-toggle>
|
||||
<mat-button-toggle (click)="onOptOut()">Maybe Later</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</mat-card>
|
|
@ -1,18 +0,0 @@
|
|||
@import "../../../../../../../../../node_modules/@angular/material/theming";
|
||||
@import "mycroft-colors";
|
||||
@import "../../../../../../../../../src/stylesheets/components/buttons";
|
||||
|
||||
.mat-h2 {
|
||||
color: mat-color($mycroft-primary);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
mat-card {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 1000px;
|
||||
}
|
||||
|
||||
mat-button-toggle-group {
|
||||
@include options-button-group
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { MatBottomSheet } from '@angular/material';
|
||||
|
||||
import { MembershipType } from '../../../../../shared/models/membership.model';
|
||||
import { ProfileService } from '../../../../../core/http/profile.service';
|
||||
import { PaymentComponent } from '../payment/payment.component';
|
||||
|
||||
@Component({
|
||||
selector: 'account-support-step',
|
||||
templateUrl: './support-step.component.html',
|
||||
styleUrls: ['./support-step.component.scss']
|
||||
})
|
||||
export class SupportStepComponent implements OnInit {
|
||||
@Input() membershipTypes: MembershipType[];
|
||||
@Input() newAcctForm: FormGroup;
|
||||
public openDatasetDescription: string[];
|
||||
public membershipDescription: string[];
|
||||
|
||||
constructor(public bottomSheet: MatBottomSheet, private profileService: ProfileService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.openDatasetDescription = [
|
||||
'Mycroft\'s voices and services can only improve with your help. ' +
|
||||
'By joining our open dataset, you agree to allow Mycroft AI to collect data related ' +
|
||||
'to your interactions with devices running Mycroft\'s voice assistant software. ' +
|
||||
'We pledge to use this contribution in a responsible way.',
|
||||
'Your data will also be made available to other researchers in the ' +
|
||||
'voice AI space with values that align with our own, like Mozilla Common Voice. ' +
|
||||
'As part of their agreement with Mycroft AI to access this data, they will be ' +
|
||||
'required to honor your request to remove any trace of your contributions if you ' +
|
||||
'decide to opt out.',
|
||||
'You can opt in or out of the open dataset at any time on your account profile page.',
|
||||
'We thank you in advance for helping to improve Mycroft\'s services!'
|
||||
];
|
||||
|
||||
this.membershipDescription = [
|
||||
'Mycroft\'s voice assistant software is open source, which means it is free to use and ' +
|
||||
'the underlying source code is available to the public. Our entire platform is free ' +
|
||||
'of advertisements. The data we collect from those that opt in to our open dataset ' +
|
||||
'is not sold to anyone.',
|
||||
'While many contributions to the Mycroft platform come in the form ' +
|
||||
'of volunteer work by community members, there is also a small team employed by Mycroft AI. ' +
|
||||
'The team curates the software, supports the community and ensures the privacy of your data. ' +
|
||||
'Your donation will help ensure our team can continue providing these services to our ' +
|
||||
'users and community.',
|
||||
'Members will receive benefits like access to premium voices.'
|
||||
];
|
||||
}
|
||||
|
||||
onOptIn() {
|
||||
this.newAcctForm.patchValue({support: {openDataset: true}});
|
||||
}
|
||||
|
||||
onOptOut() {
|
||||
this.newAcctForm.patchValue({support: {openDataset: false}});
|
||||
}
|
||||
|
||||
onMembershipSelection(membershipType: string) {
|
||||
const selectedMembership = this.membershipTypes.find(
|
||||
(membership) => membership.type === membershipType
|
||||
);
|
||||
if (selectedMembership) {
|
||||
this.openBottomSheet(selectedMembership.type);
|
||||
} else {
|
||||
this.newAcctForm.patchValue({support: {membership: null}});
|
||||
}
|
||||
}
|
||||
|
||||
openBottomSheet(selectedMembership: string) {
|
||||
const bottomSheetConfig = {
|
||||
data: {newAccount: true},
|
||||
disableClose: true,
|
||||
restoreFocus: true
|
||||
};
|
||||
const bottomSheetRef = this.bottomSheet.open(PaymentComponent, bottomSheetConfig);
|
||||
bottomSheetRef.afterDismissed().subscribe(
|
||||
(dismissValue) => {
|
||||
if (dismissValue === 'cancel') {
|
||||
this.profileService.selectedMembershipType.next('Maybe Later');
|
||||
} else {
|
||||
this.updateNewAccountForm(selectedMembership, dismissValue);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
updateNewAccountForm(selectedMembership: string, stripeToken: string) {
|
||||
console.log(stripeToken);
|
||||
this.newAcctForm.patchValue(
|
||||
{
|
||||
support: {
|
||||
membership: selectedMembership,
|
||||
paymentMethod: 'Stripe',
|
||||
paymentToken: stripeToken
|
||||
}
|
||||
}
|
||||
);
|
||||
console.log(this.newAcctForm);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,11 +1,13 @@
|
|||
<mat-card class="mat-elevation-z0">
|
||||
<h2 class="mat-h2">What should we call you?</h2>
|
||||
<p class="mat-body">{{whyUsernameParagraph}}</p>
|
||||
<mat-form-field [appearance]="'outline'">
|
||||
<mat-label>Username</mat-label>
|
||||
<input matInput required type="text" [formControl]="usernameControl">
|
||||
<mat-error *ngIf="usernameControl.hasError('required')">
|
||||
Username is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-card-title>What should we call you?</mat-card-title>
|
||||
<mat-card-content>
|
||||
<p class="mat-body">{{whyUsernameParagraph}}</p>
|
||||
<mat-form-field [appearance]="'outline'">
|
||||
<mat-label>Username</mat-label>
|
||||
<input matInput required type="text" [formControl]="usernameControl">
|
||||
<mat-error *ngIf="usernameControl.hasError('required')">
|
||||
Username is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue