Merge pull request #4 from MycroftAI/test

Test down merge to dev
pull/6/head
Chris Veilleux 2019-05-06 14:58:26 -05:00 committed by GitHub
commit 0079390753
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
203 changed files with 2988 additions and 2031 deletions

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
@import "~@angular/material/theming";
@import "../../../../../../../../../node_modules/@angular/material/theming";
@import "mycroft-colors";
mat-card {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {
}
}

View File

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

View File

@ -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() {
}
}

View File

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

View File

@ -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() {
}
}

View File

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

View File

@ -0,0 +1,3 @@
mat-form-field {
margin-left: 16px;
}

View File

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

View File

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

View File

@ -1 +1,3 @@
mat-form-field {
margin-left: 16px;
}

View File

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

View File

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

View File

@ -0,0 +1,3 @@
mat-form-field {
margin-left: 16px;
}

View File

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

View File

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

View File

@ -0,0 +1,3 @@
mat-form-field {
margin-left: 16px;
}

View File

@ -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[] {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
}
];

View File

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

View File

@ -0,0 +1,3 @@
nav {
margin-bottom: 32px;
}

View File

@ -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() {
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
<account-defaults-card
[addingDevice]="false"
[defaults]="defaults"
[defaultsForm]="defaultsForm"
>
</account-defaults-card>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
<mat-button-toggle-group
class="options-button-group"
[vertical]="alignVertical"
[value]="selectedMembershipType"
[(ngModel)]="selectedMembershipType"
(change)="onMembershipSelect($event)"
>
<mat-button-toggle

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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