commit
b2bd8d46b7
|
@ -15,12 +15,13 @@ pipeline {
|
|||
sh 'ng build --project globalnav'
|
||||
sh 'ng build --project page-not-found'
|
||||
sh 'ng build --project account --configuration development'
|
||||
sh 'ng build --project market --configuration development'
|
||||
sh 'ng build --project sso --configuration development'
|
||||
}
|
||||
}
|
||||
|
||||
// Deploy to the Test environment
|
||||
stage('Build for Test Environment') {
|
||||
stage('Build for Test') {
|
||||
when {
|
||||
branch 'test'
|
||||
}
|
||||
|
@ -31,11 +32,12 @@ pipeline {
|
|||
sh 'ng build --project globalnav'
|
||||
sh 'ng build --project page-not-found'
|
||||
sh 'ng build --project account --configuration test'
|
||||
sh 'ng build --project market --configuration test'
|
||||
sh 'ng build --project sso --configuration test'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Deploy to Test Environment') {
|
||||
stage('Deploy to Test') {
|
||||
when {
|
||||
branch 'test'
|
||||
}
|
||||
|
@ -43,22 +45,31 @@ pipeline {
|
|||
echo 'Deploying to test environment web servers...'
|
||||
withCredentials([sshUserPrivateKey(credentialsId: '6413826d-79f6-4d03-9902-ee1b73a96efd', keyFileVariable: 'JENKINS_SSH_KEY', passphraseVariable: '', usernameVariable: 'SERVER_USER')]) {
|
||||
// Deploy account application and its associated libraries
|
||||
sh 'scp -r dist/shared root@192.81.211.55:/var/www/'
|
||||
sh 'scp -r dist/globalnav root@192.81.211.55:/var/www/'
|
||||
sh 'scp -r dist/page-not-found root@192.81.211.55:/var/www/'
|
||||
sh 'scp -r dist/account root@192.81.211.55:/var/www/'
|
||||
echo 'Deploying account application...'
|
||||
sh 'scp -r dist/shared root@192.241.152.213:/var/www/'
|
||||
sh 'scp -r dist/globalnav root@192.241.152.213:/var/www/'
|
||||
sh 'scp -r dist/page-not-found root@192.241.152.213:/var/www/'
|
||||
sh 'scp -r dist/account root@192.241.152.213:/var/www/'
|
||||
|
||||
// Deploy single sign on application and its associated libraries
|
||||
echo 'Deploying single sign on application...'
|
||||
sh 'scp -r dist/shared root@198.199.90.118:/var/www/'
|
||||
sh 'scp -r dist/globalnav root@198.199.90.118:/var/www/'
|
||||
sh 'scp -r dist/page-not-found root@198.199.90.118:/var/www/'
|
||||
sh 'scp -r dist/sso root@198.199.90.118:/var/www/'
|
||||
|
||||
// Deploy single sign on application and its associated libraries
|
||||
echo 'Deploying single sign on application...'
|
||||
sh 'scp -r dist/shared root@198.211.106.110:/var/www/'
|
||||
sh 'scp -r dist/globalnav root@198.211.106.110:/var/www/'
|
||||
sh 'scp -r dist/page-not-found root@198.211.106.110:/var/www/'
|
||||
sh 'scp -r dist/market root@198.211.106.110:/var/www/'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deploy to the Production environment
|
||||
stage('Build for Production Environment') {
|
||||
stage('Build for Production') {
|
||||
when {
|
||||
branch 'master'
|
||||
}
|
||||
|
@ -73,7 +84,7 @@ pipeline {
|
|||
}
|
||||
}
|
||||
|
||||
stage('Deploy to Production Environment') {
|
||||
stage('Deploy to Production') {
|
||||
when {
|
||||
branch 'master'
|
||||
}
|
||||
|
@ -81,12 +92,14 @@ pipeline {
|
|||
echo 'Deploying to production environment web servers...'
|
||||
withCredentials([sshUserPrivateKey(credentialsId: '6413826d-79f6-4d03-9902-ee1b73a96efd', keyFileVariable: 'JENKINS_SSH_KEY', passphraseVariable: '', usernameVariable: 'SERVER_USER')]) {
|
||||
// Deploy account application and its associated libraries
|
||||
echo 'Deploying account application...'
|
||||
sh 'scp -r dist/shared root@???:/var/www/'
|
||||
sh 'scp -r dist/globalnav root@???:/var/www/'
|
||||
sh 'scp -r dist/page-not-found root@???:/var/www/'
|
||||
sh 'scp -r dist/account root@???:/var/www/'
|
||||
|
||||
// Deploy single sign on application and its associated libraries
|
||||
echo 'Deploying single sign on application...'
|
||||
sh 'scp -r dist/shared root@???:/var/www/'
|
||||
sh 'scp -r dist/globalnav root@???:/var/www/'
|
||||
sh 'scp -r dist/page-not-found root@???:/var/www/'
|
||||
|
|
40
angular.json
40
angular.json
|
@ -87,7 +87,6 @@
|
|||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "src/tsconfig.spec.json",
|
||||
"karmaConfig": "src/karma.conf.js",
|
||||
"styles": [
|
||||
"src/styles.scss",
|
||||
"src/theme.scss"
|
||||
|
@ -231,7 +230,15 @@
|
|||
"with": "projects/market/src/environments/environment.test.ts"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true
|
||||
},
|
||||
"development": {
|
||||
"fileReplacements": [
|
||||
|
@ -393,7 +400,8 @@
|
|||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"vendorChunk": true,
|
||||
"commonChunk": true,
|
||||
"buildOptimizer": true,
|
||||
"budgets": [
|
||||
{
|
||||
|
@ -410,7 +418,16 @@
|
|||
"with": "projects/sso/src/environments/environment.test.ts"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": true,
|
||||
"commonChunk": true,
|
||||
"buildOptimizer": true
|
||||
},
|
||||
"development": {
|
||||
"fileReplacements": [
|
||||
|
@ -608,6 +625,7 @@
|
|||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"commonChunk": true,
|
||||
"buildOptimizer": true,
|
||||
"budgets": [
|
||||
{
|
||||
|
@ -623,8 +641,7 @@
|
|||
"replace": "projects/account/src/environments/environment.ts",
|
||||
"with": "projects/account/src/environments/environment.dev.ts"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
]
|
||||
},
|
||||
"test": {
|
||||
"fileReplacements": [
|
||||
|
@ -633,7 +650,16 @@
|
|||
"with": "projects/account/src/environments/environment.test.ts"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": true,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": true,
|
||||
"commonChunk": true,
|
||||
"buildOptimizer": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -35,6 +35,7 @@
|
|||
"core-js": "^2.5.4",
|
||||
"font-awesome": "^4.7.0",
|
||||
"ngx-cookie-service": "^2.1.0",
|
||||
"ngx-stripe": "^7.2.0",
|
||||
"rxjs": "~6.3.3",
|
||||
"zone.js": "~0.8.26"
|
||||
},
|
||||
|
|
|
@ -1,23 +1,26 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { CreateAccountComponent } from './create-account/create-account.component';
|
||||
import { DeviceComponent } from './device/device.component';
|
||||
import { AccountResolverService } from './core/guards/account-resolver.service';
|
||||
import { DashboardComponent } from './modules/dashboard/dashboard.component';
|
||||
import { PageNotFoundComponent } from 'page-not-found';
|
||||
import { ProfileComponent } from './profile/profile.component';
|
||||
import { SkillComponent } from './skill/skill.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: 'create-account', component: CreateAccountComponent },
|
||||
{ path: 'device', component: DeviceComponent },
|
||||
{ path: 'profile', component: ProfileComponent },
|
||||
{ path: 'skill', component: SkillComponent },
|
||||
{ path: '', redirectTo: '/profile', pathMatch: 'full' },
|
||||
{ path: 'dashboard', component: DashboardComponent, resolve: {account: AccountResolverService} },
|
||||
{ path: '', redirectTo: '/dashboard', pathMatch: 'full'},
|
||||
{ path: '**', component: PageNotFoundComponent }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [ RouterModule.forRoot(routes) ],
|
||||
imports: [
|
||||
RouterModule.forRoot(
|
||||
routes,
|
||||
{
|
||||
anchorScrolling: 'enabled',
|
||||
scrollPositionRestoration: 'enabled'
|
||||
}
|
||||
)
|
||||
],
|
||||
exports: [ RouterModule ]
|
||||
})
|
||||
export class AppRoutingModule {
|
||||
|
|
|
@ -5,13 +5,13 @@ import { NgModule } from '@angular/core';
|
|||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { CreateAccountModule } from './create-account/create-account.module';
|
||||
import { DashboardModule } from './modules/dashboard/dashboard.module';
|
||||
import { GlobalnavModule } from 'globalnav';
|
||||
import { PageNotFoundModule } from 'page-not-found';
|
||||
import { DeviceModule } from './device/device.module';
|
||||
import { ProfileModule } from './profile/profile.module';
|
||||
import { DeviceModule } from './modules/device/device.module';
|
||||
import { ProfileModule } from './modules/profile/profile.module';
|
||||
import { SharedModule } from 'shared';
|
||||
import { SkillModule } from './skill/skill.module';
|
||||
import { SkillModule } from './modules/skill/skill.module';
|
||||
|
||||
@NgModule(
|
||||
{
|
||||
|
@ -19,7 +19,7 @@ import { SkillModule } from './skill/skill.module';
|
|||
imports: [
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
CreateAccountModule,
|
||||
DashboardModule,
|
||||
GlobalnavModule,
|
||||
HttpClientModule,
|
||||
DeviceModule,
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
Resolve,
|
||||
RouterStateSnapshot,
|
||||
ActivatedRouteSnapshot
|
||||
} from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { Account } from '@account/models/account.model';
|
||||
import { ProfileService } from '../http/profile.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AccountResolverService implements Resolve<Account> {
|
||||
constructor(private profileService: ProfileService) {}
|
||||
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Account> | Observable<never> {
|
||||
return this.profileService.getAccount();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
Resolve,
|
||||
RouterStateSnapshot,
|
||||
ActivatedRouteSnapshot
|
||||
} from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { DeviceService } from '../http/device.service';
|
||||
import { AccountDefaults } from '../../shared/models/defaults.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class DefaultsResolverService implements Resolve<AccountDefaults> {
|
||||
constructor(private deviceService: DeviceService) {}
|
||||
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<AccountDefaults> | Observable<never> {
|
||||
return this.deviceService.getAccountDefaults();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
Resolve,
|
||||
RouterStateSnapshot,
|
||||
ActivatedRouteSnapshot
|
||||
} from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { Device } from '../../shared/models/device.model';
|
||||
import { DeviceService } from '../http/device.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class DeviceResolverService implements Resolve<Device[]> {
|
||||
constructor(private deviceService: DeviceService) {}
|
||||
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Device[]> | Observable<never> {
|
||||
return this.deviceService.getDevices();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
Resolve,
|
||||
RouterStateSnapshot,
|
||||
ActivatedRouteSnapshot
|
||||
} from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { MembershipType } from '@account/models/membership.model';
|
||||
import { ProfileService } from '../http/profile.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class MembershipResolverService implements Resolve<MembershipType[]> {
|
||||
constructor(private profileService: ProfileService) {}
|
||||
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<MembershipType[]> | Observable<never> {
|
||||
return this.profileService.getMembershipTypes();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
Resolve,
|
||||
RouterStateSnapshot,
|
||||
ActivatedRouteSnapshot
|
||||
} from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { AccountPreferences } from '../../shared/models/preferences.model';
|
||||
import { DeviceService } from '../http/device.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PreferencesResolverService implements Resolve<AccountPreferences> {
|
||||
constructor(private deviceService: DeviceService) {}
|
||||
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<AccountPreferences> | Observable<never> {
|
||||
return this.deviceService.getAccountPreferences();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { AccountPreferences } from '@account/models/preferences.model';
|
||||
import { Device } from '@account/models/device.model';
|
||||
import { DeviceAttribute } from '@account/models/deviceAttribute.model';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { AccountDefaults } from '@account/models/defaults.model';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
const defaultsUrl = '/api/defaults';
|
||||
const deviceUrl = '/api/devices';
|
||||
const geographyUrl = 'api/geographies';
|
||||
const preferencesUrl = '/api/preferences';
|
||||
const voicesUrl = '/api/voices';
|
||||
const wakeWordUrl = '/api/wake-words';
|
||||
|
||||
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class DeviceService {
|
||||
constructor(private http: HttpClient) {
|
||||
}
|
||||
|
||||
getDevices() {
|
||||
return this.http.get<Device[]>(deviceUrl);
|
||||
}
|
||||
|
||||
addDevice(deviceForm: FormGroup) {
|
||||
this.http.post<any>(deviceUrl, deviceForm.value).subscribe();
|
||||
}
|
||||
|
||||
deleteDevice(device: Device): void {
|
||||
console.log('deleting device... ');
|
||||
}
|
||||
|
||||
addAccountPreferences(preferencesForm: FormGroup) {
|
||||
return this.http.post<any>(preferencesUrl, preferencesForm.value);
|
||||
}
|
||||
|
||||
getAccountPreferences() {
|
||||
return this.http.get<AccountPreferences>(preferencesUrl);
|
||||
}
|
||||
|
||||
updateAccountPreferences(preferencesForm: FormGroup): Observable<any> {
|
||||
return this.http.patch<any>(preferencesUrl, preferencesForm.value);
|
||||
}
|
||||
|
||||
addAccountDefaults(defaultsForm: FormGroup) {
|
||||
return this.http.post<any>(defaultsUrl, defaultsForm.value);
|
||||
}
|
||||
|
||||
updateAccountDefaults(defaultsForm: FormGroup) {
|
||||
return this.http.patch<any>(defaultsUrl, defaultsForm.value);
|
||||
}
|
||||
|
||||
getAccountDefaults() {
|
||||
return this.http.get<AccountDefaults>(defaultsUrl);
|
||||
}
|
||||
|
||||
getGeographies() {
|
||||
return this.http.get<DeviceAttribute[]>(geographyUrl);
|
||||
}
|
||||
|
||||
getVoices() {
|
||||
return this.http.get<DeviceAttribute[]>(voicesUrl);
|
||||
}
|
||||
|
||||
getWakeWords() {
|
||||
return this.http.get<DeviceAttribute[]>(wakeWordUrl);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { City } from '../../shared/models/city.model';
|
||||
import { Country } from '../../shared/models/country.model';
|
||||
import { Region } from '../../shared/models/region.model';
|
||||
import { Timezone } from '../../shared/models/timezone.model';
|
||||
|
||||
const citiesUrl = '/api/cities';
|
||||
const countriesUrl = '/api/countries';
|
||||
const regionsUrl = '/api/regions';
|
||||
const timezonesUrl = '/api/timezones';
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class GeographyService {
|
||||
constructor(private http: HttpClient) {
|
||||
}
|
||||
|
||||
getCountries() {
|
||||
return this.http.get<Country[]>(countriesUrl);
|
||||
}
|
||||
|
||||
getRegionsByCountry(country: Country) {
|
||||
const options = { params: new HttpParams().set('country', country.id) };
|
||||
return this.http.get<Region[]>(regionsUrl, options);
|
||||
}
|
||||
|
||||
getCitiesByRegion(region: Region) {
|
||||
const options = { params: new HttpParams().set('region', region.id) };
|
||||
return this.http.get<City[]>(citiesUrl, options);
|
||||
}
|
||||
|
||||
getTimezonesByCountry(country: Country) {
|
||||
const options = { params: new HttpParams().set('country', country.id) };
|
||||
return this.http.get<Timezone[]>(timezonesUrl, options);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
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 { 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(
|
||||
'redirect',
|
||||
decodeURIComponent(window.location.search).slice(10)
|
||||
);
|
||||
}
|
||||
|
||||
export function navigateToLogin(delay: number): void {
|
||||
let redirectURI = localStorage.getItem('redirect');
|
||||
localStorage.removeItem('redirect');
|
||||
if (!redirectURI) {
|
||||
redirectURI = environment.mycroftUrls.account;
|
||||
}
|
||||
const singleSignOnURI = environment.mycroftUrls.singleSignOn +
|
||||
'/login?redirect=' +
|
||||
redirectURI;
|
||||
setTimeout(() => { window.location.assign(singleSignOnURI); }, delay);
|
||||
}
|
||||
|
||||
@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.');
|
||||
}
|
||||
|
||||
handleError(error: HttpErrorResponse) {
|
||||
if (error.status === 401) {
|
||||
console.log(error);
|
||||
window.location.href = environment.mycroftUrls.singleSignOn + '/login?redirect=' + window.location.href;
|
||||
|
||||
} else 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 throwError(
|
||||
'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.
|
||||
*/
|
||||
getAccount(): Observable<Account> {
|
||||
return this.http.get<Account>(ACCOUNT_URL).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
updateAccount(accountChanges: any) {
|
||||
return this.http.patch(ACCOUNT_URL, accountChanges).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -2,45 +2,12 @@ import { Injectable } from '@angular/core';
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { Skill } from '@account/models/skill.model';
|
||||
import { SkillSettings } from '@account/models/skill-settings.model';
|
||||
|
||||
const accountSkillUrl = '/api/skills';
|
||||
const accountDeviceCountUrl = '/api/device-count';
|
||||
|
||||
export interface SelectOptions {
|
||||
display: string;
|
||||
value: string;
|
||||
}
|
||||
export interface Skill {
|
||||
id: string;
|
||||
name: string;
|
||||
hasSettings: boolean;
|
||||
}
|
||||
|
||||
export interface SettingField {
|
||||
name: string;
|
||||
type: string;
|
||||
label: string;
|
||||
options?: SelectOptions[];
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export interface SettingSection {
|
||||
name: string;
|
||||
fields: SettingField[];
|
||||
}
|
||||
|
||||
export interface SettingsDisplay {
|
||||
sections: SettingSection[];
|
||||
}
|
||||
export interface SkillSettings {
|
||||
settingsDisplay: SettingsDisplay;
|
||||
settingsValues: any;
|
||||
devices: string[];
|
||||
}
|
||||
|
||||
export interface SettingChange {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
|
@ -1,54 +0,0 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import {
|
||||
MatButtonModule,
|
||||
MatButtonToggleModule,
|
||||
MatCardModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSnackBarModule,
|
||||
MatStepperModule
|
||||
} from '@angular/material';
|
||||
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
|
||||
import { AuthenticationStepComponent } from './authentication-step/authentication-step.component';
|
||||
import { AgreementStepComponent } from './agreement-step/agreement-step.component';
|
||||
import { CreateAccountComponent } from './create-account.component';
|
||||
import { CreateAccountService } from './create-account.service';
|
||||
import { UsernameStepComponent } from './username-step/username-step.component';
|
||||
import { SharedModule } from 'shared';
|
||||
import { SupportStepComponent } from './support-step/support-step.component';
|
||||
import { DoneStepComponent } from './done-step/done-step.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CreateAccountComponent,
|
||||
AgreementStepComponent,
|
||||
UsernameStepComponent,
|
||||
AuthenticationStepComponent,
|
||||
SupportStepComponent,
|
||||
DoneStepComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FontAwesomeModule,
|
||||
FlexLayoutModule,
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
MatButtonToggleModule,
|
||||
MatCardModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSnackBarModule,
|
||||
MatStepperModule,
|
||||
ReactiveFormsModule,
|
||||
SharedModule,
|
||||
],
|
||||
providers: [
|
||||
CreateAccountService
|
||||
]
|
||||
})
|
||||
export class CreateAccountModule { }
|
|
@ -1,83 +0,0 @@
|
|||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
|
||||
import { throwError } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
const accountUrl = '/api/account';
|
||||
const agreementUrl = '/api/agreement/';
|
||||
const fiveSeconds = 5000;
|
||||
|
||||
export interface Agreement {
|
||||
type: string;
|
||||
version: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export function storeRedirect() {
|
||||
localStorage.setItem(
|
||||
'redirect',
|
||||
decodeURIComponent(window.location.search).slice(10)
|
||||
);
|
||||
}
|
||||
|
||||
export function navigateToLogin(delay: number): void {
|
||||
const redirectURI = localStorage.getItem('redirect');
|
||||
const singleSignOnURI = environment.mycroftUrls.singleSignOn +
|
||||
'/login?redirect=' +
|
||||
redirectURI;
|
||||
localStorage.removeItem('redirect');
|
||||
setTimeout(() => { window.location.assign(singleSignOnURI); }, delay);
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CreateAccountService {
|
||||
|
||||
constructor(private http: HttpClient, private errorSnackbar: MatSnackBar) {
|
||||
}
|
||||
|
||||
handleError(error: HttpErrorResponse) {
|
||||
if (error.status === 400) {
|
||||
this.errorSnackbar.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.');
|
||||
}
|
||||
|
||||
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>(agreementUrl + url_suffix);
|
||||
}
|
||||
|
||||
addAccount(newAcctForm: FormGroup) {
|
||||
return this.http.post<any>(accountUrl, newAcctForm.value).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
<div mat-dialog-title class="mat-h2-primary">{{dialogTitle}}</div>
|
||||
|
||||
<div mat-dialog-content>
|
||||
<div class="mat-body">{{dialogInstructions}}</div>
|
||||
<mat-radio-group fxLayout="column" [(ngModel)]="dialogData">
|
||||
<ng-container *ngFor="let possibleValue of possibleValues">
|
||||
|
||||
<!-- Radio buttons for pre-defined placements -->
|
||||
<mat-radio-button *ngIf="possibleValue.preDefined" class="predefined-group" [value]="possibleValue.name">
|
||||
{{possibleValue.name}}
|
||||
</mat-radio-button>
|
||||
|
||||
<!-- Radio buttons for user-defined placements -->
|
||||
<div *ngIf="!possibleValue.preDefined" fxLayout="row" fxLayoutAlign="space-between center">
|
||||
<mat-radio-button [value]="possibleValue.name">
|
||||
<mat-form-field [floatLabel]="'never'">
|
||||
<input matInput class="user-defined-group" value="{{possibleValue.name}}">
|
||||
</mat-form-field>
|
||||
</mat-radio-button>
|
||||
<button mat-icon-button [disableRipple]="true">
|
||||
<fa-icon [icon]="deleteIcon"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<!-- Radio button to add a new user-defined group -->
|
||||
<mat-radio-button>
|
||||
<mat-form-field [floatLabel]="'never'">
|
||||
<input matInput placeholder="Add {{dialogTitle}}">
|
||||
</mat-form-field>
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
|
||||
<div mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onCancelClick()">CANCEL</button>
|
||||
<button mat-button class="attribute-save-button" [mat-dialog-close]="dialogData">SAVE</button>
|
||||
</div>
|
|
@ -1,22 +0,0 @@
|
|||
@import '~@angular/material/theming';
|
||||
@import '~src/stylesheets/mycroft-colors';
|
||||
@import '~src/stylesheets/components/buttons';
|
||||
|
||||
.predefined-group {
|
||||
margin-bottom: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.mat-body{
|
||||
margin-bottom: 16px;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
fa-icon {
|
||||
color: mat-color($mycroft-warn);
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.attribute-save-button {
|
||||
@include action-button-primary;
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import {Component, Input, OnInit} from '@angular/core';
|
||||
import { MatDialogRef } from '@angular/material';
|
||||
|
||||
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { DeviceAttribute } from '../device.service';
|
||||
import { GeographyEditComponent } from './geography/geography-edit.component';
|
||||
import { GroupEditComponent } from './group/group-edit.component';
|
||||
import { PlacementEditComponent } from './placement/placement-edit.component';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'account-device-attribute-edit',
|
||||
templateUrl: './attr-edit.component.html',
|
||||
styleUrls: ['./attr-edit.component.scss']
|
||||
})
|
||||
export class AttrEditComponent implements OnInit {
|
||||
@Input() dialogData: string;
|
||||
@Input() dialogInstructions: string;
|
||||
@Input() dialogRef: MatDialogRef<GeographyEditComponent | GroupEditComponent | PlacementEditComponent>;
|
||||
@Input() dialogTitle: string;
|
||||
@Input() possibleValues: DeviceAttribute[];
|
||||
public deleteIcon = faTrashAlt;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
onCancelClick(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
<mat-form-field [appearance]="'outline'" (click)="onClick()">
|
||||
<mat-label>{{label}}</mat-label>
|
||||
<div fxLayout="row" fxLayoutAlign="none center">
|
||||
<input
|
||||
matInput
|
||||
[readonly]="true"
|
||||
type="text"
|
||||
value="{{value.name}}"
|
||||
>
|
||||
<fa-icon matSuffix [icon]="editIcon" style="font-size: 16px;"></fa-icon>
|
||||
</div>
|
||||
<mat-hint>{{hint}}</mat-hint>
|
||||
</mat-form-field>
|
|
@ -1,9 +0,0 @@
|
|||
mat-form-field {
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
fa-icon {
|
||||
padding-right: 10px
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material';
|
||||
|
||||
import { faCaretRight } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
@Component({
|
||||
selector: 'account-device-attribute-view',
|
||||
templateUrl: './attr-view.component.html',
|
||||
styleUrls: ['./attr-view.component.scss']
|
||||
})
|
||||
export class AttrViewComponent implements OnInit {
|
||||
@Input() editDialog: any;
|
||||
@Input() hint: string;
|
||||
@Input() label: string;
|
||||
@Input() possibleValues: any[];
|
||||
@Input() value: any;
|
||||
public editIcon = faCaretRight;
|
||||
|
||||
constructor(private dialog: MatDialog) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
onClick() {
|
||||
const dialogRef = this.dialog.open(this.editDialog, { data: this.value.name });
|
||||
dialogRef.afterClosed().subscribe(
|
||||
(result) => { this.updateDevice(result); }
|
||||
);
|
||||
}
|
||||
|
||||
updateDevice(newValue: string) {
|
||||
if (newValue) {
|
||||
this.possibleValues.forEach(
|
||||
(value) => {
|
||||
if (value.name === newValue) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
<account-device-attribute-edit
|
||||
[dialogData]="data"
|
||||
[dialogInstructions]="dialogInstructions"
|
||||
[dialogRef]="dialogRef"
|
||||
[dialogTitle]="'Geography'"
|
||||
[possibleValues]="deviceGeographies"
|
||||
>
|
||||
</account-device-attribute-edit>
|
||||
<!--<div mat-dialog-title class="mat-h2-primary">Geography</div>-->
|
||||
|
||||
<!--<div mat-dialog-content>-->
|
||||
<!--<div class="mat-body">-->
|
||||
<!--Groups are useful to organize multiple devices. You can reuse device names if they are in different groups.-->
|
||||
<!--</div>-->
|
||||
<!--<mat-radio-group fxLayout="column" [(ngModel)]="data">-->
|
||||
<!--<ng-container *ngFor="let geo of deviceGeographies">-->
|
||||
<!--<!– Radio buttons for user-defined groups –>-->
|
||||
<!--<div fxLayout="row" fxLayoutAlign="space-between center">-->
|
||||
<!--<mat-radio-button [value]="geo.name">-->
|
||||
<!--<mat-form-field [floatLabel]="'never'">-->
|
||||
<!--<input matInput value="{{geo.name}}">-->
|
||||
<!--</mat-form-field>-->
|
||||
<!--</mat-radio-button>-->
|
||||
<!--<button mat-icon-button [disableRipple]="true">-->
|
||||
<!--<fa-icon class="danger-icon" [icon]="deleteIcon"></fa-icon>-->
|
||||
<!--</button>-->
|
||||
<!--</div>-->
|
||||
|
||||
<!--</ng-container>-->
|
||||
|
||||
<!--<!– Radio button to add a new user-defined group –>-->
|
||||
<!--<mat-radio-button>-->
|
||||
<!--<mat-form-field [floatLabel]="'never'">-->
|
||||
<!--<input matInput placeholder="Add Geographic Location">-->
|
||||
<!--</mat-form-field>-->
|
||||
<!--</mat-radio-button>-->
|
||||
<!--</mat-radio-group>-->
|
||||
<!--</div>-->
|
||||
|
||||
<!--<div mat-dialog-actions align="end">-->
|
||||
<!--<button mat-button (click)="onCancelClick()">CANCEL</button>-->
|
||||
<!--<button mat-button [mat-dialog-close]="data" class="action-button">SAVE</button>-->
|
||||
<!--</div>-->
|
|
@ -1,25 +0,0 @@
|
|||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
|
||||
|
||||
import { DeviceAttribute, DeviceService} from '../../device.service';
|
||||
|
||||
@Component({
|
||||
selector: 'account-device-geography-edit',
|
||||
templateUrl: './geography-edit.component.html',
|
||||
styleUrls: ['./geography-edit.component.scss']
|
||||
})
|
||||
export class GeographyEditComponent implements OnInit {
|
||||
public deviceGeographies: DeviceAttribute[];
|
||||
public dialogInstructions = '';
|
||||
|
||||
constructor(
|
||||
private deviceService: DeviceService,
|
||||
public dialogRef: MatDialogRef<GeographyEditComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: string) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.deviceGeographies = this.deviceService.deviceGeographies;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
<account-device-attribute-view
|
||||
[editDialog]="dialog"
|
||||
[hint]="'Country, postal code, time zone'"
|
||||
[label]="'Geography'"
|
||||
[possibleValues]="deviceGeographies"
|
||||
[value]="device.location"
|
||||
>
|
||||
</account-device-attribute-view>
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
|
||||
import { Device, DeviceAttribute, DeviceService } from '../../device.service';
|
||||
import { GeographyEditComponent } from './geography-edit.component';
|
||||
|
||||
@Component({
|
||||
selector: 'account-device-geography-view',
|
||||
templateUrl: './geography-view.component.html',
|
||||
styleUrls: ['./geography-view.component.scss']
|
||||
})
|
||||
export class GeographyViewComponent implements OnInit {
|
||||
@Input() device: Device;
|
||||
public deviceGeographies: DeviceAttribute[];
|
||||
public dialog = GeographyEditComponent;
|
||||
|
||||
constructor( private service: DeviceService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.deviceGeographies = this.service.deviceGeographies;
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
<account-device-attribute-edit
|
||||
[dialogData]="data"
|
||||
[dialogInstructions]="dialogInstructions"
|
||||
[dialogRef]="dialogRef"
|
||||
[dialogTitle]="'Group'"
|
||||
[possibleValues]="deviceGroups"
|
||||
>
|
||||
</account-device-attribute-edit>
|
|
@ -1,25 +0,0 @@
|
|||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
|
||||
|
||||
import { DeviceAttribute, DeviceService} from '../../device.service';
|
||||
|
||||
@Component({
|
||||
selector: 'account-device-group-edit',
|
||||
templateUrl: './group-edit.component.html',
|
||||
styleUrls: ['./group-edit.component.scss']
|
||||
})
|
||||
export class GroupEditComponent implements OnInit {
|
||||
public deviceGroups: DeviceAttribute[];
|
||||
public dialogInstructions = 'Groups are useful to organize multiple ' +
|
||||
'devices. You can reuse device names if they are in different groups.';
|
||||
|
||||
constructor(
|
||||
private deviceService: DeviceService,
|
||||
public dialogRef: MatDialogRef<GroupEditComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: DeviceAttribute) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.deviceGroups = this.deviceService.deviceGroups;
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
<account-device-attribute-view
|
||||
[editDialog]="dialog"
|
||||
[hint]="'Mechanism to categorize devices'"
|
||||
[label]="'Group'"
|
||||
[possibleValues]="deviceGroups"
|
||||
[value]="device.group"
|
||||
>
|
||||
</account-device-attribute-view>
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
|
||||
import { Device, DeviceAttribute, DeviceService } from '../../device.service';
|
||||
import { GroupEditComponent } from './group-edit.component';
|
||||
|
||||
@Component({
|
||||
selector: 'account-device-group-view',
|
||||
templateUrl: './group-view.component.html',
|
||||
styleUrls: ['./group-view.component.scss']
|
||||
})
|
||||
export class GroupViewComponent implements OnInit {
|
||||
@Input() device: Device;
|
||||
public deviceGroups: DeviceAttribute[];
|
||||
public dialog = GroupEditComponent;
|
||||
|
||||
constructor( private service: DeviceService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.deviceGroups = this.service.deviceGroups;
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
<account-device-attribute-edit
|
||||
[dialogData]="data"
|
||||
[dialogInstructions]="dialogInstructions"
|
||||
[dialogRef]="dialogRef"
|
||||
[dialogTitle]="'Placement'"
|
||||
[possibleValues]="devicePlacements"
|
||||
>
|
||||
</account-device-attribute-edit>
|
|
@ -1,26 +0,0 @@
|
|||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
|
||||
|
||||
import { DeviceAttribute, DeviceService} from '../../device.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'account-device-placement-edit',
|
||||
templateUrl: './placement-edit.component.html',
|
||||
styleUrls: ['./placement-edit.component.scss']
|
||||
})
|
||||
export class PlacementEditComponent implements OnInit {
|
||||
public devicePlacements: DeviceAttribute[];
|
||||
public dialogInstructions = 'You can optionally indicate where a device is ' +
|
||||
'placed within a location. Field is informational only.';
|
||||
|
||||
constructor(
|
||||
private deviceService: DeviceService,
|
||||
public dialogRef: MatDialogRef<PlacementEditComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: string) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.devicePlacements = this.deviceService.devicePlacements;
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
<account-device-attribute-view
|
||||
[editDialog]="dialog"
|
||||
[hint]="'Where a device is placed within a location'"
|
||||
[label]="'Placement'"
|
||||
[possibleValues]="devicePlacements"
|
||||
[value]="device.placement"
|
||||
>
|
||||
</account-device-attribute-view>
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
|
||||
import {Device, DeviceAttribute, DeviceService} from '../../device.service';
|
||||
import { PlacementEditComponent } from './placement-edit.component';
|
||||
|
||||
@Component({
|
||||
selector: 'account-device-placement-view',
|
||||
templateUrl: './placement-view.component.html',
|
||||
styleUrls: ['./placement-view.component.scss']
|
||||
})
|
||||
export class PlacementViewComponent implements OnInit {
|
||||
@Input() device: Device;
|
||||
public devicePlacements: DeviceAttribute[];
|
||||
public dialog = PlacementEditComponent;
|
||||
|
||||
constructor( private service: DeviceService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.devicePlacements = this.service.devicePlacements;
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
<account-device-attribute-edit
|
||||
[dialogData]="data"
|
||||
[dialogInstructions]="dialogInstructions"
|
||||
[dialogRef]="dialogRef"
|
||||
[dialogTitle]="'Voice'"
|
||||
[possibleValues]="deviceVoices"
|
||||
>
|
||||
</account-device-attribute-edit>
|
|
@ -1,27 +0,0 @@
|
|||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
|
||||
|
||||
import { DeviceAttribute, DeviceService} from '../../device.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'account-device-voice-edit',
|
||||
templateUrl: './voice-edit.component.html',
|
||||
styleUrls: ['./voice-edit.component.scss']
|
||||
})
|
||||
export class VoiceEditComponent implements OnInit {
|
||||
public deviceVoices: DeviceAttribute[];
|
||||
public dialogInstructions = 'Mycroft\'s voice technology is rapidly ' +
|
||||
'evolving. Local voices guarantee the most privacy. Premium voices ' +
|
||||
'are more natural but require an internet connection.';
|
||||
|
||||
constructor(
|
||||
private deviceService: DeviceService,
|
||||
public dialogRef: MatDialogRef<VoiceEditComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: string) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.deviceVoices = this.deviceService.deviceVoices;
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
<account-device-attribute-view
|
||||
[editDialog]="dialog"
|
||||
[hint]="'Select the voice used by your device'"
|
||||
[label]="'Voice'"
|
||||
[possibleValues]="deviceVoices"
|
||||
[value]="device.voice"
|
||||
>
|
||||
</account-device-attribute-view>
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
|
||||
import {Device, DeviceAttribute, DeviceService} from '../../device.service';
|
||||
import { VoiceEditComponent } from './voice-edit.component';
|
||||
|
||||
@Component({
|
||||
selector: 'account-device-voice-view',
|
||||
templateUrl: './voice-view.component.html',
|
||||
styleUrls: ['./voice-view.component.scss']
|
||||
})
|
||||
export class VoiceViewComponent implements OnInit {
|
||||
@Input() device: Device;
|
||||
public deviceVoices: DeviceAttribute[];
|
||||
public dialog = VoiceEditComponent;
|
||||
|
||||
constructor( private service: DeviceService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.deviceVoices = this.service.deviceVoices;
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
<account-device-attribute-edit
|
||||
[dialogData]="data"
|
||||
[dialogInstructions]="dialogInstructions"
|
||||
[dialogRef]="dialogRef"
|
||||
[dialogTitle]="'Wake Word'"
|
||||
[possibleValues]="deviceWakeWords"
|
||||
>
|
||||
</account-device-attribute-edit>
|
|
@ -1,27 +0,0 @@
|
|||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
|
||||
|
||||
import { DeviceAttribute, DeviceService} from '../../device.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'account-device-wake-word-edit',
|
||||
templateUrl: './wake-word-edit.component.html',
|
||||
styleUrls: ['./wake-word-edit.component.scss']
|
||||
})
|
||||
export class WakeWordEditComponent implements OnInit {
|
||||
public deviceWakeWords: DeviceAttribute[];
|
||||
public dialogInstructions = 'Mycroft\'s voice technology is rapidly ' +
|
||||
'evolving. Local voices guarantee the most privacy. Premium voices ' +
|
||||
'are more natural but require an internet connection.';
|
||||
|
||||
constructor(
|
||||
private deviceService: DeviceService,
|
||||
public dialogRef: MatDialogRef<WakeWordEditComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: string) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.deviceWakeWords = this.deviceService.deviceWakeWords;
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
<account-device-attribute-view
|
||||
[editDialog]="dialog"
|
||||
[hint]="'Select the wake word used by your device'"
|
||||
[label]="'Wake Word'"
|
||||
[possibleValues]="deviceWakeWords"
|
||||
[value]="device.wakeWord"
|
||||
>
|
||||
</account-device-attribute-view>
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
|
||||
import {Device, DeviceAttribute, DeviceService} from '../../device.service';
|
||||
import { WakeWordEditComponent} from './wake-word-edit.component';
|
||||
|
||||
@Component({
|
||||
selector: 'account-device-wake-word-view',
|
||||
templateUrl: './wake-word-view.component.html',
|
||||
styleUrls: ['./wake-word-view.component.scss']
|
||||
})
|
||||
export class WakeWordViewComponent implements OnInit {
|
||||
@Input() device: Device;
|
||||
public deviceWakeWords: DeviceAttribute[];
|
||||
public dialog = WakeWordEditComponent;
|
||||
|
||||
constructor( private service: DeviceService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.deviceWakeWords = this.service.deviceVoices;
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
<div id="add-device-button" fxLayout="row" fxLayoutAlign="start center">
|
||||
<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>
|
||||
|
||||
<!-- Device listing - show summary in expansion panel header and editable fields in expansion panel detail -->
|
||||
<mat-expansion-panel *ngFor="let device of devices">
|
||||
|
||||
<!-- Put the platform icon, device name and device placement in the panel header -->
|
||||
<mat-expansion-panel-header [expandedHeight]="'100px'" [collapsedHeight]="'100px'">
|
||||
<div fxLayout="row" fxLayoutAlign="start" fxLayoutGap="16px">
|
||||
<img [src]="getDeviceIcon(device)"/>
|
||||
<div>
|
||||
<div class="mat-h2">{{device.name}}</div>
|
||||
<div class="mat-subheader">{{device.placement.name}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div id="device-settings" fxLayout.xs="row wrap">
|
||||
<div fxLayout="row wrap" fxLayoutGap.gt-xs="16px">
|
||||
<!-- Navigation to skill settings for this device. -->
|
||||
<mat-form-field fxFlex.xl=40 fxFlex.lt-xl [appearance]="'outline'">
|
||||
<mat-label>Name</mat-label>
|
||||
<input
|
||||
id="deviceName"
|
||||
matInput
|
||||
name="deviceName"
|
||||
required
|
||||
type="text"
|
||||
value="{{device.name}}"
|
||||
>
|
||||
<mat-hint>Must be unique within a device group (if defined)</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
<account-device-group-view fxFlex.xl=40 fxFlex.lt-xl [device]="device"></account-device-group-view>
|
||||
<account-device-geography-view fxFlex.xl=40 fxFlex.lt-xl [device]="device"></account-device-geography-view>
|
||||
<account-device-placement-view fxFlex.xl=40 fxFlex.lt-xl [device]="device"></account-device-placement-view>
|
||||
<account-device-voice-view fxFlex.xl=40 fxFlex.lt-xl [device]="device"></account-device-voice-view>
|
||||
<account-device-wake-word-view fxFlex.xl=40 fxFlex.lt-xl [device]="device"></account-device-wake-word-view>
|
||||
</div>
|
||||
|
||||
<div fxFlex.xl="25" fxFlex.lt-xl>
|
||||
<div fxLayout="column">
|
||||
<!-- Static fields that display platform, software version and hardware version -->
|
||||
<div>
|
||||
<div *ngFor="let staticData of defineStaticDeviceFields(device)">
|
||||
<span class="mat-body">{{staticData.name}}: </span>
|
||||
<span class="mat-body-primary">{{staticData.value}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Last but not least, the delete device button -->
|
||||
<button mat-flat-button color="primary" class="settings-button">
|
||||
<fa-icon [icon]="settingsIcon"></fa-icon>
|
||||
SKILL SETTINGS
|
||||
</button>
|
||||
|
||||
<button mat-flat-button color="warn" class="delete-button" (click)="onRemovalClick(device)">
|
||||
<fa-icon [icon]="deleteIcon"></fa-icon>
|
||||
REMOVE DEVICE
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
|
@ -1,10 +0,0 @@
|
|||
<div fxLayout="row" fxLayoutAlign="center center">
|
||||
<mat-tab-group mat-stretch-tabs>
|
||||
<mat-tab [label]="'Devices'">
|
||||
<account-device-list></account-device-list>
|
||||
</mat-tab>
|
||||
<mat-tab [label]="'Preferences'">
|
||||
<account-device-preferences></account-device-preferences>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
|
@ -1,56 +0,0 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material';
|
||||
|
||||
import { faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { DeviceService, Device } from './device.service';
|
||||
import { RemoveComponent } from './remove/remove.component';
|
||||
|
||||
@Component({
|
||||
selector: 'account-device',
|
||||
templateUrl: './device.component.html',
|
||||
styleUrls: ['./device.component.scss']
|
||||
})
|
||||
export class DeviceComponent implements OnInit {
|
||||
public addIcon = faPlus;
|
||||
public deleteIcon = faTrash;
|
||||
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'},
|
||||
'picroft': {icon: '../assets/picroft-icon.svg', displayName: 'Picroft'},
|
||||
'kde': {icon: '../assets/kde-icon.svg', displayName: 'KDE'}
|
||||
};
|
||||
private selectedDevice: Device;
|
||||
|
||||
constructor(public dialog: MatDialog, private deviceService: DeviceService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.devices = this.deviceService.devices;
|
||||
}
|
||||
|
||||
onRemovalClick (device: Device) {
|
||||
const removalDialogRef = this.dialog.open(RemoveComponent, {data: false});
|
||||
this.selectedDevice = device;
|
||||
removalDialogRef.afterClosed().subscribe(
|
||||
(result) => {
|
||||
if (result) { this.deviceService.deleteDevice(device); }
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
defineStaticDeviceFields(device: Device) {
|
||||
const knownPlatform = this.platforms[device.platform];
|
||||
return [
|
||||
{name: 'Platform', value: knownPlatform ? knownPlatform.displayName : device.platform},
|
||||
{name: 'Core Version', value: device.coreVersion},
|
||||
{name: 'Enclosure Version', value: device.enclosureVersion}
|
||||
];
|
||||
}
|
||||
|
||||
getDeviceIcon(device: Device) {
|
||||
const knownPlatform = this.platforms[device.platform];
|
||||
// TODO: get unknown product icon from design team.
|
||||
return knownPlatform ? knownPlatform.icon : '../assets/mark-1-icon.svg';
|
||||
}
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import {
|
||||
MatButtonModule,
|
||||
MatButtonToggleModule,
|
||||
MatCardModule,
|
||||
MatDialogModule,
|
||||
MatExpansionModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatRadioModule,
|
||||
MatSelectModule,
|
||||
MatSlideToggleModule,
|
||||
MatTabsModule,
|
||||
MatToolbarModule
|
||||
} from '@angular/material';
|
||||
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
|
||||
import { AttrEditComponent } from './attribute/attr-edit.component';
|
||||
import { AttrViewComponent } from './attribute/attr-view.component';
|
||||
import { DeviceComponent } from './device.component';
|
||||
import { DeviceListComponent } from './device-list/device-list.component';
|
||||
import { DeviceService } from './device.service';
|
||||
import { GeographyEditComponent } from './attribute/geography/geography-edit.component';
|
||||
import { GeographyViewComponent } from './attribute/geography/geography-view.component';
|
||||
import { GroupEditComponent } from './attribute/group/group-edit.component';
|
||||
import { GroupViewComponent } from './attribute/group/group-view.component';
|
||||
import { PlacementEditComponent } from './attribute/placement/placement-edit.component';
|
||||
import { PlacementViewComponent } from './attribute/placement/placement-view.component';
|
||||
import { RemoveComponent } from './remove/remove.component';
|
||||
import { VoiceEditComponent } from './attribute/voice/voice-edit.component';
|
||||
import { VoiceViewComponent } from './attribute/voice/voice-view.component';
|
||||
import { PreferencesComponent } from './preferences/preferences.component';
|
||||
import { WakeWordEditComponent } from './attribute/wake-word/wake-word-edit.component';
|
||||
import { WakeWordViewComponent } from './attribute/wake-word/wake-word-view.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AttrEditComponent,
|
||||
AttrViewComponent,
|
||||
DeviceComponent,
|
||||
DeviceListComponent,
|
||||
GeographyEditComponent,
|
||||
GeographyViewComponent,
|
||||
GroupEditComponent,
|
||||
GroupViewComponent,
|
||||
PlacementEditComponent,
|
||||
PlacementViewComponent,
|
||||
RemoveComponent,
|
||||
VoiceEditComponent,
|
||||
VoiceViewComponent,
|
||||
PreferencesComponent,
|
||||
WakeWordEditComponent,
|
||||
WakeWordViewComponent
|
||||
],
|
||||
entryComponents: [
|
||||
GeographyEditComponent,
|
||||
GroupEditComponent,
|
||||
PlacementEditComponent,
|
||||
RemoveComponent,
|
||||
VoiceEditComponent,
|
||||
WakeWordEditComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
DragDropModule,
|
||||
FlexLayoutModule,
|
||||
FontAwesomeModule,
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
MatButtonToggleModule,
|
||||
MatCardModule,
|
||||
MatDialogModule,
|
||||
MatExpansionModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatRadioModule,
|
||||
MatSelectModule,
|
||||
MatSlideToggleModule,
|
||||
MatTabsModule,
|
||||
MatToolbarModule
|
||||
],
|
||||
providers: [
|
||||
DeviceService
|
||||
]
|
||||
})
|
||||
export class DeviceModule { }
|
|
@ -1,113 +0,0 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
export interface DeviceAttribute {
|
||||
id?: string;
|
||||
name: string;
|
||||
preDefined: boolean;
|
||||
}
|
||||
|
||||
export interface Device {
|
||||
coreVersion: string;
|
||||
enclosureVersion: string;
|
||||
group: DeviceAttribute;
|
||||
id: string;
|
||||
location: DeviceAttribute;
|
||||
name: string;
|
||||
placement: DeviceAttribute;
|
||||
platform: string;
|
||||
voice: DeviceAttribute;
|
||||
wakeWord: DeviceAttribute;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DeviceService {
|
||||
public devices: Device[] = [
|
||||
{
|
||||
coreVersion: '18.08',
|
||||
enclosureVersion: '1.2.3',
|
||||
group: {id: '1', name: 'None', preDefined: true},
|
||||
id: 'abc-def-ghi',
|
||||
location: {id: '1a2b-3c4d-5e6f', name: 'United States, 64101, CST', preDefined: false},
|
||||
name: 'Mark',
|
||||
placement: {id: 'bbb-bbb-bbb', name: 'Living Room', preDefined: false},
|
||||
platform: 'mark-one',
|
||||
voice: {id: '1a2b-3c4d-5e6f', name: 'British Male', preDefined: true},
|
||||
wakeWord: {id: '1a2b-3c4d-5e6f', name: 'Hey Mycroft', preDefined: true},
|
||||
},
|
||||
{
|
||||
coreVersion: '18.08',
|
||||
enclosureVersion: '1.2.3',
|
||||
group: {id: '1', name: 'None', preDefined: true},
|
||||
id: 'bcd-efg-hij',
|
||||
location: {id: '1a2b-3c4d-5e6f', name: 'United States, 64101, CST', preDefined: false},
|
||||
name: 'Marky Mark',
|
||||
placement: {id: 'bbb-bbb-bbb', name: 'Kitchen', preDefined: true},
|
||||
platform: 'mark-two',
|
||||
voice: {id: '1a2b-3c4d-5e6f', name: 'British Male', preDefined: true},
|
||||
wakeWord: {id: 'a1b2-c3d4-e5f6', name: 'Christopher', preDefined: true}
|
||||
},
|
||||
{
|
||||
coreVersion: '18.08',
|
||||
enclosureVersion: '1.2.3',
|
||||
group: {id: '2', name: 'Parent House', preDefined: false},
|
||||
id: 'cde-fgh-ijk',
|
||||
location: {id: '1a2b-3c4d-5e6f', name: 'United States, 64101, CST', preDefined: false},
|
||||
name: 'American Pie',
|
||||
placement: {id: 'ddd-ddd-ddd', name: 'Bedroom', preDefined: true},
|
||||
platform: 'picroft',
|
||||
voice: {id: '1a2b-3c4d-5e6f', name: 'British Male', preDefined: true},
|
||||
wakeWord: {id: 'a1b2-c3d4-e5f6', name: 'Christopher', preDefined: true}
|
||||
},
|
||||
{
|
||||
coreVersion: '18.08',
|
||||
enclosureVersion: '1.2.3',
|
||||
group: {id: '2', name: 'Parent House', preDefined: false},
|
||||
id: 'def-ghi-jkl',
|
||||
location: {id: '1a2b-3c4d-5e6f', name: 'United States, 64101, CST', preDefined: false},
|
||||
name: 'Kappa Delta Epsilon',
|
||||
placement: {id: 'fff-fff-fff', name: 'Kitchen', preDefined: true},
|
||||
platform: 'kde',
|
||||
voice: {id: '1a2b-3c4d-5e6f', name: 'British Male', preDefined: true},
|
||||
wakeWord: {id: 'abcd-efgh-ijkl', name: 'Hey Jarvis', preDefined: true}
|
||||
}
|
||||
];
|
||||
|
||||
public deviceGroups: DeviceAttribute[] = [
|
||||
{ id: '1', name: 'None', preDefined: true},
|
||||
{ id: null, name: 'Home', preDefined: true},
|
||||
{ id: null, name: 'Office', preDefined: true},
|
||||
{ id: '2', name: 'Parent House', preDefined: false}
|
||||
];
|
||||
|
||||
public devicePlacements: DeviceAttribute[] = [
|
||||
{ id: '1', name: 'None', preDefined: true},
|
||||
{ id: null, name: 'Bedroom', preDefined: true},
|
||||
{ id: null, name: 'Kitchen', preDefined: true},
|
||||
{ id: '2', name: 'Living Room', preDefined: false}
|
||||
];
|
||||
|
||||
public deviceGeographies: DeviceAttribute[] = [
|
||||
{id: '1a2b-3c4d-5e6f', name: 'United States, 64101, CST', preDefined: false},
|
||||
{id: 'a1b2-c3d4-e5f6', name: 'United Kingdom, ABCDE, BST', preDefined: false}
|
||||
];
|
||||
|
||||
public deviceVoices: DeviceAttribute[] = [
|
||||
{id: '1a2b-3c4d-5e6f', name: 'British Male', preDefined: true},
|
||||
{id: 'a1b2-c3d4-e5f6', name: 'American Female', preDefined: true},
|
||||
{id: 'abcd-efgh-ijkl', name: 'American Male', preDefined: true}
|
||||
];
|
||||
|
||||
public deviceWakeWords: DeviceAttribute[] = [
|
||||
{id: '1a2b-3c4d-5e6f', name: 'Hey Mycroft', preDefined: true},
|
||||
{id: 'a1b2-c3d4-e5f6', name: 'Christopher', preDefined: true},
|
||||
{id: 'abcd-efgh-ijkl', name: 'Hey Jarvis', preDefined: true}
|
||||
];
|
||||
|
||||
constructor() { }
|
||||
|
||||
deleteDevice(device: Device): void {
|
||||
console.log('deleting device... ');
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
<mat-card id="basic-settings-card">
|
||||
<mat-toolbar>
|
||||
<span class="section-card-title">Basic Settings</span>
|
||||
</mat-toolbar>
|
||||
<div fxLayout="row wrap" fxLayoutAlign="space-between center" class="section-content">
|
||||
<mat-slide-toggle [labelPosition]="'before'">Use device groups</mat-slide-toggle>
|
||||
<span>
|
||||
<mat-label>Measurement System</mat-label>
|
||||
<mat-button-toggle-group>
|
||||
<mat-button-toggle>Imperial</mat-button-toggle>
|
||||
<mat-button-toggle>Metric</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</span>
|
||||
<span>
|
||||
<mat-label>Time Format</mat-label>
|
||||
<mat-button-toggle-group>
|
||||
<mat-button-toggle>12 Hour</mat-button-toggle>
|
||||
<mat-button-toggle>24 Hour</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</span>
|
||||
<span>
|
||||
<mat-label>Date Format</mat-label>
|
||||
<mat-button-toggle-group>
|
||||
<mat-button-toggle>DD/MM/YYYY</mat-button-toggle>
|
||||
<mat-button-toggle>MM/DD/YYYY</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</span>
|
||||
</div>
|
||||
</mat-card>
|
||||
|
||||
<mat-card id="default-settings-card">
|
||||
<mat-toolbar>
|
||||
<span>Defaults</span>
|
||||
</mat-toolbar>
|
||||
<div fxLayout="column" fxLayoutAlign="start start" class="section-content">
|
||||
<mat-label>Default Location</mat-label>
|
||||
<mat-select placeholder="None Selected"></mat-select>
|
||||
</div>
|
||||
</mat-card>
|
||||
|
||||
<mat-card id="advanced-settings-card">
|
||||
<mat-toolbar>
|
||||
<span class="section-card-title">Advanced Settings</span>
|
||||
</mat-toolbar>
|
||||
<div fxLayout="column" class="section-content">
|
||||
<div class="mat-body" *ngFor="let paragraph of advancedSettingsDesc">
|
||||
<p>{{paragraph}}</p>
|
||||
</div>
|
||||
<button mat-flat-button>VIEW DOCUMENTATION</button>
|
||||
</div>
|
||||
</mat-card>
|
|
@ -1,33 +0,0 @@
|
|||
@import "~src/stylesheets/components/buttons";
|
||||
@import "~src/stylesheets/components/cards";
|
||||
|
||||
#basic-settings-card {
|
||||
@include section-card;
|
||||
margin-top: 16px;
|
||||
|
||||
mat-button-toggle-group {
|
||||
@include options-button-group;
|
||||
|
||||
mat-button-toggle {
|
||||
width: 130px;
|
||||
}
|
||||
}
|
||||
|
||||
mat-label {
|
||||
margin-bottom: 8px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
#default-settings-card {
|
||||
@include section-card;
|
||||
}
|
||||
#advanced-settings-card {
|
||||
@include section-card;
|
||||
button {
|
||||
@include action-button-primary;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'account-device-preferences',
|
||||
templateUrl: './preferences.component.html',
|
||||
styleUrls: ['./preferences.component.scss']
|
||||
})
|
||||
export class PreferencesComponent implements OnInit {
|
||||
public 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.'
|
||||
];
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
<div id="dashboard" fxLayout="row" fxLayoutAlign="center" fxLayout.xs="column" fxLayoutGap="16px">
|
||||
<mat-card *ngIf="account.membership" fxFlex="40">
|
||||
<mat-card-header>
|
||||
<fa-icon [icon]="awardIcon"></fa-icon>
|
||||
</mat-card-header>
|
||||
<mat-card-title>Mycroft likes you</mat-card-title>
|
||||
<mat-card-content>
|
||||
<p class="mat-body">
|
||||
Your support makes a big difference. Thanks to you and other Mycroft supporters,
|
||||
we can continue building upon the Mycroft experience. It is also motivating
|
||||
knowing there is a community of people who believe in the same things we do.
|
||||
</p>
|
||||
<p class="mat-body">
|
||||
We keep brainstorming and working on benefits to offer you as a supporter.
|
||||
For now, you can change your Mycroft’s voice to a higher quality one.
|
||||
</p>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<a mat-button [href]="voicesUrl">CHECK OUT PREMIUM VOICES</a>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
|
||||
<mat-card *ngIf="!account.membership" fxFlex="40">
|
||||
<mat-card-header>
|
||||
<fa-icon [icon]="awardIcon"></fa-icon>
|
||||
</mat-card-header>
|
||||
<mat-card-title>Become a Member</mat-card-title>
|
||||
<mat-card-content>
|
||||
<p class="mat-body">
|
||||
For less than one cup of coffee a month, you can help support our server costs,
|
||||
hosting, and ongoing development. All of which all improve Mycroft's performance
|
||||
and user experience.
|
||||
</p>
|
||||
<p class="mat-body">
|
||||
Your membership also gets you exclusive access to premium voices,
|
||||
along with new features that are being prepared for the second half of 2019.
|
||||
</p>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<a mat-button [href]="accountUrl">MONTHLY MEMBERSHIP $1.99</a>
|
||||
<a mat-button [href]="accountUrl">YEARLY MEMBERSHIP $19.99</a>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
|
||||
<mat-card fxFlex="40">
|
||||
<mat-card-header>
|
||||
<fa-icon [icon]="downloadIcon"></fa-icon>
|
||||
</mat-card-header>
|
||||
<mat-card-title>Install New Skills</mat-card-title>
|
||||
<mat-card-content>
|
||||
<p class="mat-body">
|
||||
Your device comes with a set of pre-installed skills like Timers and Wikipedia.
|
||||
To change the settings of your skills, for instance the sound of your alarm,
|
||||
go to Skill Settings.
|
||||
</p>
|
||||
<p class="mat-body">
|
||||
Our open source community extends the functionality of Mycroft. Their efforts have
|
||||
produced skills like Fairytalez and ISS Tracker. To install new skills,
|
||||
visit the Marketplace.
|
||||
</p>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<a mat-button [href]="marketplaceUrl">SKILL MARKETPLACE</a>
|
||||
<a mat-button [href]="settingsUrl">SKILL SETTINGS</a>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</div>
|
|
@ -0,0 +1,48 @@
|
|||
@import "~@angular/material/theming";
|
||||
@import "mycroft-colors";
|
||||
@import "components/buttons";
|
||||
iframe {
|
||||
min-width: 350px;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#dashboard {
|
||||
max-width: 1200px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
mat-card {
|
||||
min-height: 540px;
|
||||
margin: 16px;
|
||||
max-width: 540px;
|
||||
min-width: 300px;
|
||||
|
||||
fa-icon {
|
||||
font-size: 60px;
|
||||
color: mat-color($mycroft-primary)
|
||||
}
|
||||
|
||||
mat-card-title {
|
||||
font-size: 40px;
|
||||
font-weight: bolder;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
mat-card-content {
|
||||
padding: 16px;
|
||||
|
||||
.mat-body {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
mat-card-actions {
|
||||
padding: 16px;
|
||||
|
||||
a {
|
||||
@include action-button-primary;
|
||||
margin: 8px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { faAward, faDownload } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { Account } from '@account/models/account.model';
|
||||
import { environment } from '@account/environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'account-dashboard',
|
||||
templateUrl: './dashboard.component.html',
|
||||
styleUrls: ['./dashboard.component.scss']
|
||||
})
|
||||
export class DashboardComponent implements OnInit {
|
||||
public account: Account;
|
||||
public awardIcon = faAward;
|
||||
public downloadIcon = faDownload;
|
||||
public marketplaceUrl = environment.mycroftUrls.marketplace;
|
||||
public settingsUrl = environment.mycroftUrls.account + '/skills';
|
||||
public voicesUrl = environment.mycroftUrls.mimic;
|
||||
public accountUrl = environment.mycroftUrls.account + '/profile';
|
||||
|
||||
constructor(private route: ActivatedRoute) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe(
|
||||
(data: {account: Account}) => { this.account = data.account; }
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { MatButtonModule, MatCardModule } from '@angular/material';
|
||||
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
|
||||
import { DashboardComponent } from './dashboard.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
DashboardComponent
|
||||
],
|
||||
entryComponents: [
|
||||
DashboardComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FlexLayoutModule,
|
||||
FontAwesomeModule,
|
||||
MatButtonModule,
|
||||
MatCardModule
|
||||
]
|
||||
})
|
||||
export class DashboardModule { }
|
|
@ -0,0 +1,18 @@
|
|||
<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>
|
|
@ -0,0 +1,93 @@
|
|||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { startWith, map, tap} from 'rxjs/operators';
|
||||
|
||||
import { City } from '../../../../../shared/models/city.model';
|
||||
import { AbstractControl, FormGroup, ValidatorFn } from '@angular/forms';
|
||||
|
||||
@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;
|
||||
@Input() required: boolean;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.cityControl = this.deviceForm.controls['city'];
|
||||
this.cityControl.disable();
|
||||
}
|
||||
|
||||
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(); })
|
||||
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private filterCities(value: string): City[] {
|
||||
const filterValue = value.toLowerCase();
|
||||
let filteredCities: City[];
|
||||
|
||||
if (this.cities) {
|
||||
filteredCities = this.cities.filter(
|
||||
(cty) => cty.name.toLowerCase().includes(filterValue)
|
||||
);
|
||||
} else {
|
||||
filteredCities = [];
|
||||
}
|
||||
|
||||
return filteredCities;
|
||||
}
|
||||
|
||||
cityValidator(): ValidatorFn {
|
||||
return (cityControl: AbstractControl) => {
|
||||
let valid = true;
|
||||
if (cityControl.value) {
|
||||
const foundCity = this.cities.find(
|
||||
(city) => city.name === cityControl.value
|
||||
);
|
||||
if (!foundCity) {
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
return valid ? null : {cityNotFound: true};
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
checkForValidCity() {
|
||||
if (this.cityControl.valid) {
|
||||
if (this.cityControl.value) {
|
||||
const foundCity = this.cities.find(
|
||||
(city) => city.name === this.cityControl.value
|
||||
);
|
||||
this.citySelected.emit(foundCity);
|
||||
} else {
|
||||
this.citySelected.emit(null);
|
||||
}
|
||||
} else {
|
||||
this.citySelected.emit(null);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<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>
|
|
@ -0,0 +1,90 @@
|
|||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { AbstractControl, FormGroup, ValidatorFn } from '@angular/forms';
|
||||
import { map, startWith, tap } from 'rxjs/operators';
|
||||
|
||||
import { Country } from '@account/models/country.model';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'account-country-input',
|
||||
templateUrl: './country-input.component.html',
|
||||
styleUrls: ['./country-input.component.scss']
|
||||
})
|
||||
export class CountryInputComponent implements OnInit {
|
||||
@Input() countries$: Observable<Country[]>;
|
||||
private countries: Country[];
|
||||
private countryControl: AbstractControl;
|
||||
@Output() countrySelected = new EventEmitter<Country>();
|
||||
@Input() deviceForm: FormGroup;
|
||||
public filteredCountries$: Observable<Country[]>;
|
||||
@Input() required: boolean;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
this.countryControl = this.deviceForm.controls['country'];
|
||||
}
|
||||
|
||||
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(); })
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private filterCountries(value: string): Country[] {
|
||||
const filterValue = value.toLowerCase();
|
||||
let filteredCountries: Country[];
|
||||
|
||||
if (this.countries) {
|
||||
filteredCountries = this.countries.filter(
|
||||
(country) => country.name.toLowerCase().includes(filterValue)
|
||||
);
|
||||
} else {
|
||||
filteredCountries = [];
|
||||
}
|
||||
|
||||
return filteredCountries;
|
||||
}
|
||||
|
||||
countryValidator(): ValidatorFn {
|
||||
return (countryControl: AbstractControl) => {
|
||||
let valid = true;
|
||||
if (countryControl.value) {
|
||||
const foundCountry = this.countries.find(
|
||||
(country) => country.name === countryControl.value
|
||||
);
|
||||
if (!foundCountry) {
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
return valid ? null : {countryNotFound: true};
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
checkForValidCountry() {
|
||||
if (this.countryControl.valid) {
|
||||
if (this.countryControl.value) {
|
||||
const foundCountry = this.countries.find(
|
||||
(country) => country.name === this.countryControl.value
|
||||
);
|
||||
this.countrySelected.emit(foundCountry);
|
||||
} else {
|
||||
this.countrySelected.emit(null);
|
||||
}
|
||||
} else {
|
||||
this.countrySelected.emit(null);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<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>
|
|
@ -0,0 +1,90 @@
|
|||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { map, startWith, tap } from 'rxjs/operators';
|
||||
import { Region } from '../../../../../shared/models/region.model';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AbstractControl, FormGroup, ValidatorFn } from '@angular/forms';
|
||||
|
||||
@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>();
|
||||
@Input() required: boolean;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
this.regionControl = this.deviceForm.controls['region'];
|
||||
this.regionControl.disable();
|
||||
}
|
||||
|
||||
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(); })
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private filterRegions(value: string): Region[] {
|
||||
const filterValue = value.toLowerCase();
|
||||
let filteredRegions: Region[];
|
||||
|
||||
if (this.regions) {
|
||||
filteredRegions = this.regions.filter(
|
||||
(region) => region.name.toLowerCase().includes(filterValue)
|
||||
);
|
||||
} else {
|
||||
filteredRegions = [];
|
||||
}
|
||||
|
||||
return filteredRegions;
|
||||
}
|
||||
|
||||
regionValidator(): ValidatorFn {
|
||||
return (regionControl: AbstractControl) => {
|
||||
let valid = true;
|
||||
if (regionControl.value) {
|
||||
const foundRegion = this.regions.find(
|
||||
(region) => region.name === regionControl.value
|
||||
);
|
||||
if (!foundRegion) {
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
return valid ? null : {regionNotFound: true};
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
checkForValidRegion() {
|
||||
if (this.regionControl.valid) {
|
||||
if (this.regionControl.value) {
|
||||
const foundRegion = this.regions.find(
|
||||
(region) => region.name === this.regionControl.value
|
||||
);
|
||||
this.regionSelected.emit(foundRegion);
|
||||
} else {
|
||||
this.regionSelected.emit(null);
|
||||
}
|
||||
} else {
|
||||
this.regionSelected.emit(null);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<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>
|
|
@ -0,0 +1,75 @@
|
|||
import { Component, Input, OnInit, Output } from '@angular/core';
|
||||
import { AbstractControl, FormGroup, ValidatorFn } from '@angular/forms';
|
||||
import { Observable } from 'rxjs';
|
||||
import { startWith, map, tap } from 'rxjs/operators';
|
||||
|
||||
import { Timezone } from '../../../../../shared/models/timezone.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[]>();
|
||||
@Input() required: boolean;
|
||||
@Input() timezones$: Observable<Timezone[]>;
|
||||
private timezones: Timezone[];
|
||||
private timezoneControl: AbstractControl;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.timezoneControl = this.deviceForm.controls['timezone'];
|
||||
this.timezoneControl.disable();
|
||||
}
|
||||
|
||||
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)),
|
||||
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private filterTimezones(value: string): Timezone[] {
|
||||
const filterValue = value.toLowerCase();
|
||||
let filteredTimezones: Timezone[];
|
||||
|
||||
if (this.timezones) {
|
||||
filteredTimezones = this.timezones.filter(
|
||||
(tz) => tz.name.toLowerCase().includes(filterValue)
|
||||
);
|
||||
} else {
|
||||
filteredTimezones = [];
|
||||
}
|
||||
|
||||
return filteredTimezones;
|
||||
}
|
||||
|
||||
timezoneValidator(): ValidatorFn {
|
||||
return (timezoneControl: AbstractControl) => {
|
||||
let valid = true;
|
||||
if (timezoneControl.value) {
|
||||
const foundTimezone = this.timezones.find(
|
||||
(timezone) => timezone.name === timezoneControl.value
|
||||
);
|
||||
if (!foundTimezone) {
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
return valid ? null : {timezoneNotFound: true};
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<mat-card class="mat-elevation-z0">
|
||||
<mat-card-title>Your device is ready!</mat-card-title>
|
||||
<mat-card-content>
|
||||
<h2 class="mat-h2">Here are some example questions and commands:</h2>
|
||||
<h3 class="mat-subheading-2">{{wakeWord}}...</h3>
|
||||
<p class="mat-body">Who is Abraham Lincoln?</p>
|
||||
<p class="mat-body">What is the latest news?</p>
|
||||
<p class="mat-body">Set a timer for ten minutes.</p>
|
||||
<p class="mat-body">Set an alarm for eight o'clock tomorrow morning.</p>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
|
@ -0,0 +1,20 @@
|
|||
@import "~@angular/material/theming";
|
||||
@import "mycroft-colors";
|
||||
|
||||
mat-card {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 500px;
|
||||
|
||||
mat-card-title {
|
||||
color: mat-color($mycroft-primary)
|
||||
}
|
||||
|
||||
.mat-subheading-2 {
|
||||
color: mat-color($mycroft-accent, A700)
|
||||
}
|
||||
|
||||
p {
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'account-device-add-complete',
|
||||
templateUrl: './add-complete.component.html',
|
||||
styleUrls: ['./add-complete.component.scss']
|
||||
})
|
||||
export class AddCompleteComponent implements OnInit {
|
||||
@Input() wakeWord: string;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
<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>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
@import "~@angular/material/theming";
|
||||
@import "mycroft-colors";
|
||||
@import "components/buttons";
|
||||
@import "components/cards";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
max-width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
mat-card-actions {
|
||||
button {
|
||||
@include action-button-primary;
|
||||
margin-bottom: 16px;
|
||||
margin-right: 16px;
|
||||
|
||||
&:disabled {
|
||||
background-color: mat-color($mycroft-accent, 200);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
<mat-tab-group>
|
||||
<mat-tab label="Configuration">
|
||||
<mat-card-content fxLayout="row wrap">
|
||||
<account-display-field
|
||||
fxFlex="50"
|
||||
[label]="'Voice'"
|
||||
[value]="device.voice.name"
|
||||
>
|
||||
</account-display-field>
|
||||
<account-display-field
|
||||
fxFlex="50"
|
||||
[label]="'Wake Word'"
|
||||
[value]="device.wakeWord.name"
|
||||
>
|
||||
</account-display-field>
|
||||
<account-display-field
|
||||
fxFlex="33"
|
||||
[label]="'Platform'"
|
||||
[value]="device.platform"
|
||||
>
|
||||
</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>
|
||||
</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"
|
||||
>
|
||||
</account-display-field>
|
||||
<account-display-field
|
||||
fxFlex="50"
|
||||
[label]="'Time Zone'"
|
||||
[value]="device.geography.timezone"
|
||||
>
|
||||
</account-display-field>
|
||||
<account-display-field
|
||||
fxFlex="50"
|
||||
[label]="'Region'"
|
||||
[value]="device.geography.region"
|
||||
>
|
||||
</account-display-field>
|
||||
<account-display-field
|
||||
fxFlex="50"
|
||||
[label]="'City'"
|
||||
[value]="device.geography.city"
|
||||
>
|
||||
</account-display-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Placement</mat-label>
|
||||
<input matInput readonly type="text" value="{{device.placement}}">
|
||||
<mat-hint>e.g. Kitchen, Bedroom, Office</mat-hint>
|
||||
</mat-form-field>
|
||||
</mat-card-content>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
|
@ -0,0 +1,3 @@
|
|||
mat-card-content {
|
||||
margin-top: 32px;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
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() {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
<div id="add-device-button" fxLayout="row" fxLayoutAlign="start center" routerLink="/devices/add">
|
||||
<img src="../assets/generic-device-icon-blue.svg">
|
||||
<span fxFlex class="mat-h2">ADD DEVICE</span>
|
||||
<fa-icon class="mat-h2" [icon]="addIcon"></fa-icon>
|
||||
</div>
|
||||
|
||||
<div fxLayout="row wrap" fxLayoutGap.gt-xs="16px">
|
||||
<mat-card *ngFor="let device of devices">
|
||||
<mat-card-title fxLayout="row" fxLayoutAlign="start center">
|
||||
<img [src]="getDeviceIcon(device)"/>
|
||||
{{device.name}}
|
||||
</mat-card-title>
|
||||
<account-device-info [device]="device"></account-device-info>
|
||||
<!--<mat-card-actions>-->
|
||||
<!--<button mat-flat-button color="warn" (click)="onRemovalClick(device)">-->
|
||||
<!--REMOVE-->
|
||||
<!--</button>-->
|
||||
<!--<button mat-flat-button color="primary" (click)="onDeviceEdit(device)">-->
|
||||
<!--EDIT-->
|
||||
<!--</button>-->
|
||||
<!--</mat-card-actions>-->
|
||||
</mat-card>
|
||||
</div>
|
|
@ -1,77 +1,70 @@
|
|||
@import '~@angular/material/theming';
|
||||
@import "~@angular/material/theming";
|
||||
@import '~src/stylesheets/mycroft-colors';
|
||||
@import '~src/stylesheets/components/buttons';
|
||||
|
||||
@mixin panel-defaults {
|
||||
border-radius: 12px;
|
||||
max-width: 1000px;
|
||||
min-width: 250px;
|
||||
width: 330px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
#add-device-button {
|
||||
@include action-button-primary;
|
||||
@include panel-defaults;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
height: 50px;
|
||||
margin-top: 32px;
|
||||
|
||||
img {
|
||||
height: 32px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
.mat-h2 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
fa-icon {
|
||||
color: mat-color($mycroft-accent, 'A200');
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
mat-expansion-panel {
|
||||
@include panel-defaults;
|
||||
margin-top: 16px;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
fa-icon {
|
||||
padding-right: 10px
|
||||
}
|
||||
|
||||
img {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
//.delete-button {
|
||||
// margin-top: 20px;
|
||||
//}
|
||||
|
||||
.mat-body-primary {
|
||||
margin-bottom: 0;
|
||||
height: 32px;
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.mat-h2 {
|
||||
color: mat-color($mycroft-primary);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mat-subheader {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
mat-card {
|
||||
@include panel-defaults;
|
||||
|
||||
button {
|
||||
margin-bottom: 8px;
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
margin-bottom: 20px;
|
||||
mat-card-title {
|
||||
color: mat-color($mycroft-primary, 700);
|
||||
font-weight: bold;
|
||||
|
||||
img {
|
||||
height: 60px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
mat-divider {
|
||||
margin-left: 16px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
mat-tab-group {
|
||||
height: 280px;
|
||||
|
||||
mat-card-content {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,10 +1,12 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
import { faCog, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { DeviceService, Device } from '../device.service';
|
||||
import { RemoveComponent } from '../remove/remove.component';
|
||||
import { DeviceService } from '@account/http/device.service';
|
||||
import { Device } from '@account/models/device.model';
|
||||
import { RemoveComponent } from '../../../remove/remove.component';
|
||||
|
||||
@Component({
|
||||
selector: 'account-device-list',
|
||||
|
@ -13,7 +15,6 @@ import { RemoveComponent } from '../remove/remove.component';
|
|||
})
|
||||
export class DeviceListComponent implements OnInit {
|
||||
public addIcon = faPlus;
|
||||
public deleteIcon = faTrash;
|
||||
public devices: Device[];
|
||||
public platforms = {
|
||||
'mark-one': {icon: '../assets/mark-1-icon.svg', displayName: 'Mark I'},
|
||||
|
@ -22,15 +23,21 @@ export class DeviceListComponent implements OnInit {
|
|||
'kde': {icon: '../assets/kde-icon.svg', displayName: 'KDE'}
|
||||
};
|
||||
private selectedDevice: Device;
|
||||
public settingsIcon = faCog;
|
||||
|
||||
constructor(public dialog: MatDialog, private deviceService: DeviceService) { }
|
||||
constructor(
|
||||
public dialog: MatDialog,
|
||||
private deviceService: DeviceService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.devices = this.deviceService.devices;
|
||||
this.route.data.subscribe(
|
||||
(data: {devices: Device[]}) => { this.devices = data.devices; }
|
||||
);
|
||||
}
|
||||
|
||||
onRemovalClick (device: Device) {
|
||||
onRemovalClick(device: Device) {
|
||||
const removalDialogRef = this.dialog.open(RemoveComponent, {data: false});
|
||||
this.selectedDevice = device;
|
||||
removalDialogRef.afterClosed().subscribe(
|
||||
|
@ -40,13 +47,13 @@ export class DeviceListComponent implements OnInit {
|
|||
);
|
||||
}
|
||||
|
||||
defineStaticDeviceFields(device: Device) {
|
||||
onDeviceEdit(device: Device) {
|
||||
this.router.navigate(['/devices', device.id]);
|
||||
}
|
||||
|
||||
getPlatform(device: Device) {
|
||||
const knownPlatform = this.platforms[device.platform];
|
||||
return [
|
||||
{name: 'Platform', value: knownPlatform ? knownPlatform.displayName : device.platform},
|
||||
{name: 'Core Version', value: device.coreVersion},
|
||||
{name: 'Enclosure Version', value: device.enclosureVersion}
|
||||
];
|
||||
return knownPlatform ? knownPlatform.displayName : device.platform;
|
||||
}
|
||||
|
||||
getDeviceIcon(device: Device) {
|
|
@ -0,0 +1,43 @@
|
|||
<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>-->
|
|
@ -0,0 +1,36 @@
|
|||
@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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
|
||||
import { AccountPreferences } from '@account/models/preferences.model';
|
||||
import { DeviceService } from '../../../../../core/http/device.service';
|
||||
import { OptionButtonsConfig } from '@account/models/option-buttons-config.model';
|
||||
|
||||
@Component({
|
||||
selector: 'account-device-preferences',
|
||||
templateUrl: './preferences.component.html',
|
||||
styleUrls: ['./preferences.component.scss']
|
||||
})
|
||||
export class PreferencesComponent implements OnInit {
|
||||
public advancedSettingsDesc: string[];
|
||||
@Input() deviceSetup: boolean;
|
||||
@Input() preferences: AccountPreferences;
|
||||
@Input() preferencesForm: FormGroup;
|
||||
public measurementOptionsConfig: OptionButtonsConfig;
|
||||
public timeFormatOptionsConfig: OptionButtonsConfig;
|
||||
public dateFormatOptionsConfig: OptionButtonsConfig;
|
||||
|
||||
constructor(private deviceService: DeviceService) {
|
||||
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() {
|
||||
this.buildAdvancedSettingsDesc();
|
||||
}
|
||||
|
||||
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});
|
||||
}
|
||||
|
||||
onSave() {
|
||||
if (this.preferences) {
|
||||
this.deviceService.updateAccountPreferences(this.preferencesForm).subscribe(
|
||||
() => { this.preferences = this.preferencesForm.value; }
|
||||
);
|
||||
} else {
|
||||
this.deviceService.addAccountPreferences(this.preferencesForm).subscribe(
|
||||
() => { this.preferences = this.preferencesForm.value; }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
<!--
|
||||
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>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
@import "~@angular/material/theming";
|
||||
@import "mycroft-colors";
|
||||
@import "components/buttons";
|
||||
@import "components/cards";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
max-width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
mat-card-actions {
|
||||
button {
|
||||
@include action-button-primary;
|
||||
margin-bottom: 16px;
|
||||
margin-right: 16px;
|
||||
|
||||
&:disabled {
|
||||
background-color: mat-color($mycroft-accent, 200);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
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; }
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
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 { DeviceResolverService } from '../../core/guards/device-resolver.service';
|
||||
import { PreferencesResolverService } from '../../core/guards/preferences-resolver.service';
|
||||
|
||||
const deviceRoutes: Routes = [
|
||||
{
|
||||
path: 'devices',
|
||||
component: DevicesComponent,
|
||||
resolve: {
|
||||
defaults: DefaultsResolverService,
|
||||
devices: DeviceResolverService,
|
||||
preferences: PreferencesResolverService
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'devices/add',
|
||||
component: DeviceAddComponent,
|
||||
resolve: {
|
||||
defaults: DefaultsResolverService,
|
||||
preferences: PreferencesResolverService
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'devices/:device_id',
|
||||
component: DeviceEditComponent,
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(deviceRoutes)
|
||||
],
|
||||
exports: [
|
||||
RouterModule
|
||||
]
|
||||
})
|
||||
export class DeviceRoutingModule { }
|
|
@ -0,0 +1,82 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import {
|
||||
MatAutocompleteModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatDialogModule,
|
||||
MatExpansionModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatRadioModule,
|
||||
MatStepperModule,
|
||||
MatTabsModule,
|
||||
MatToolbarModule
|
||||
} from '@angular/material';
|
||||
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
|
||||
import { AddCompleteComponent } from './components/view/add-complete/add-complete.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 { 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 { RegionInputComponent } from './components/input/region-input/region-input.component';
|
||||
import { RemoveComponent } from './remove/remove.component';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { TimezoneInputComponent } from './components/input/timezone-input/timezone-input.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddCompleteComponent,
|
||||
CityInputComponent,
|
||||
CountryInputComponent,
|
||||
DefaultsComponent,
|
||||
DeviceAddComponent,
|
||||
DeviceEditComponent,
|
||||
DeviceInfoComponent,
|
||||
DeviceListComponent,
|
||||
DevicesComponent,
|
||||
PreferencesComponent,
|
||||
RegionInputComponent,
|
||||
RemoveComponent,
|
||||
TimezoneInputComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
DeviceAddComponent,
|
||||
RemoveComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FlexLayoutModule,
|
||||
FontAwesomeModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MatAutocompleteModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatDialogModule,
|
||||
MatExpansionModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatRadioModule,
|
||||
MatStepperModule,
|
||||
MatTabsModule,
|
||||
MatToolbarModule,
|
||||
SharedModule,
|
||||
DeviceRoutingModule
|
||||
],
|
||||
providers: [
|
||||
DeviceService
|
||||
]
|
||||
})
|
||||
export class DeviceModule { }
|
|
@ -0,0 +1,129 @@
|
|||
<!-- Show a horizontal stepper on larger devices -->
|
||||
<mat-horizontal-stepper *ngIf="!alignVertical" labelPosition="bottom">
|
||||
<!-- Use font awesome icons in the stepper to indicate progress -->
|
||||
<ng-template matStepperIcon="done">
|
||||
<fa-icon [icon]="stepDoneIcon"></fa-icon>
|
||||
</ng-template>
|
||||
<ng-template matStepperIcon="edit">
|
||||
<fa-icon [icon]="stepDoneIcon"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<mat-step label="Preferences" *ngIf="!preferences" [stepControl]="preferencesForm">
|
||||
<form [formGroup]="preferencesForm" (ngSubmit)="onPreferencesSubmit()">
|
||||
<account-device-preferences
|
||||
[preferencesForm]="preferencesForm"
|
||||
[deviceSetup]="true"
|
||||
>
|
||||
</account-device-preferences>
|
||||
<div fxLayout="row" fxLayoutAlign="end">
|
||||
<button mat-button matStepperNext [disabled]="!preferencesForm.valid">
|
||||
NEXT
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
|
||||
<mat-step label="Defaults" *ngIf="!preferences" [stepControl]="defaultsForm">
|
||||
<form [formGroup]="defaultsForm" (ngSubmit)="onDefaultsSubmit()">
|
||||
<account-device-edit
|
||||
[deviceForm]="defaultsForm"
|
||||
[action]="'default setup'"
|
||||
>
|
||||
</account-device-edit>
|
||||
<div fxLayout="row" fxLayoutAlign="end">
|
||||
<button mat-button matStepperNext [disabled]="!defaultsForm.valid">
|
||||
NEXT
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
|
||||
<mat-step label="Device" [stepControl]="deviceForm">
|
||||
<form [formGroup]="deviceForm" (ngSubmit)="onDeviceSubmit()">
|
||||
<account-device-edit
|
||||
[deviceForm]="deviceForm"
|
||||
[action]="'device setup'"
|
||||
[defaults]="defaults"
|
||||
>
|
||||
</account-device-edit>
|
||||
<div fxLayout="row" fxLayoutAlign="end">
|
||||
<button mat-button matStepperNext [disabled]="!deviceForm.valid">
|
||||
NEXT
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
|
||||
<mat-step label="Done!">
|
||||
<account-device-add-complete [wakeWord]="deviceForm.controls['wakeWord'].value">
|
||||
</account-device-add-complete>
|
||||
<div fxLayout="row" fxLayoutAlign="end">
|
||||
<button mat-button (click)="onFinished()">FINISHED</button>
|
||||
</div>
|
||||
</mat-step>
|
||||
</mat-horizontal-stepper>
|
||||
|
||||
<!-- Show a vertical stepper on smaller devices -->
|
||||
<mat-vertical-stepper *ngIf="alignVertical">
|
||||
<!-- Use font awesome icons in the stepper to indicate progress -->
|
||||
<ng-template matStepperIcon="done">
|
||||
<fa-icon [icon]="stepDoneIcon"></fa-icon>
|
||||
</ng-template>
|
||||
<ng-template matStepperIcon="edit">
|
||||
<fa-icon [icon]="stepDoneIcon"></fa-icon>
|
||||
</ng-template>
|
||||
|
||||
<mat-step label="Preferences" *ngIf="!preferences" [stepControl]="preferencesForm">
|
||||
<form [formGroup]="preferencesForm" (ngSubmit)="onPreferencesSubmit()">
|
||||
<account-device-preferences
|
||||
[preferencesForm]="preferencesForm"
|
||||
[deviceSetup]="true"
|
||||
>
|
||||
</account-device-preferences>
|
||||
<div fxLayout="row" fxLayoutAlign="end">
|
||||
<button mat-button matStepperNext [disabled]="!preferencesForm.valid">
|
||||
NEXT
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
|
||||
<mat-step label="Defaults" *ngIf="!preferences" [stepControl]="defaultsForm">
|
||||
<form [formGroup]="defaultsForm" (ngSubmit)="onDefaultsSubmit()">
|
||||
<account-device-edit
|
||||
[deviceForm]="defaultsForm"
|
||||
[action]="'default setup'"
|
||||
>
|
||||
</account-device-edit>
|
||||
<div fxLayout="row" fxLayoutAlign="end">
|
||||
<button mat-button matStepperNext [disabled]="!defaultsForm.valid">
|
||||
NEXT
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
|
||||
<mat-step label="Device" [stepControl]="deviceForm">
|
||||
<form [formGroup]="deviceForm" (ngSubmit)="onDeviceSubmit()">
|
||||
<account-device-edit
|
||||
[deviceForm]="deviceForm"
|
||||
[action]="'device setup'"
|
||||
[defaults]="defaults"
|
||||
>
|
||||
</account-device-edit>
|
||||
<div fxLayout="row" fxLayoutAlign="end">
|
||||
<button mat-button matStepperNext [disabled]="!deviceForm.valid">
|
||||
NEXT
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
|
||||
<mat-step label="Done!">
|
||||
<account-device-add-complete [wakeWord]="deviceForm.controls['wakeWord'].value">
|
||||
</account-device-add-complete>
|
||||
<div fxLayout="row" fxLayoutAlign="end">
|
||||
<button mat-button (click)="onFinished()">FINISHED</button>
|
||||
</div>
|
||||
</mat-step>
|
||||
</mat-vertical-stepper>
|
|
@ -0,0 +1,17 @@
|
|||
@import "~@angular/material/theming";
|
||||
@import "mycroft-colors";
|
||||
@import "components/buttons";
|
||||
|
||||
button {
|
||||
@include action-button-primary;
|
||||
|
||||
&:disabled {
|
||||
background-color: mat-color($mycroft-accent, 200);
|
||||
}
|
||||
}
|
||||
|
||||
mat-horizontal-stepper {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 800px;
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { MediaChange, MediaObserver } from '@angular/flex-layout';
|
||||
import {FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { AccountDefaults } from '@account/models/defaults.model';
|
||||
import { AccountPreferences } from '@account/models/preferences.model';
|
||||
import { DeviceService } from '@account/http/device.service';
|
||||
|
||||
@Component({
|
||||
selector: 'account-device-add',
|
||||
templateUrl: './device-add.component.html',
|
||||
styleUrls: ['./device-add.component.scss']
|
||||
})
|
||||
export class DeviceAddComponent implements OnInit {
|
||||
public alignVertical: boolean;
|
||||
public defaults: AccountDefaults;
|
||||
public defaultsForm: FormGroup;
|
||||
public deviceForm: FormGroup;
|
||||
private mediaWatcher: Subscription;
|
||||
public preferencesForm: FormGroup;
|
||||
public preferences: AccountPreferences;
|
||||
public stepDoneIcon = faCheck;
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
public mediaObserver: MediaObserver,
|
||||
private deviceService: DeviceService,
|
||||
private route: ActivatedRoute
|
||||
) {
|
||||
this.mediaWatcher = mediaObserver.media$.subscribe(
|
||||
(change: MediaChange) => {
|
||||
this.alignVertical = ['xs', 'sm'].includes(change.mqAlias);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.getResolverData();
|
||||
this.buildForms();
|
||||
}
|
||||
|
||||
private buildForms() {
|
||||
this.preferencesForm = this.formBuilder.group(
|
||||
{
|
||||
dateFormat: [null, Validators.required],
|
||||
measurementSystem: [null, Validators.required],
|
||||
timeFormat: [null, Validators.required],
|
||||
}
|
||||
);
|
||||
|
||||
// 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],
|
||||
voice: [null]
|
||||
}
|
||||
);
|
||||
this.deviceForm = this.formBuilder.group(
|
||||
{
|
||||
city: [null, Validators.required],
|
||||
name: [null, Validators.required],
|
||||
country: [null, Validators.required],
|
||||
pairingCode: [
|
||||
null,
|
||||
[
|
||||
Validators.required,
|
||||
Validators.maxLength(6),
|
||||
Validators.minLength(6)
|
||||
]
|
||||
],
|
||||
placement: [null],
|
||||
region: [null, Validators.required],
|
||||
timezone: [null, Validators.required],
|
||||
wakeWord: [null, Validators.required],
|
||||
voice: [null, Validators.required]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getResolverData() {
|
||||
this.route.data.subscribe(
|
||||
(data: {defaults: AccountDefaults, preferences: AccountPreferences}) => {
|
||||
this.preferences = data.preferences;
|
||||
this.defaults = data.defaults;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onDeviceSubmit() {
|
||||
this.deviceService.addDevice(this.deviceForm);
|
||||
}
|
||||
|
||||
onPreferencesSubmit() {
|
||||
this.deviceService.addAccountPreferences(this.preferencesForm).subscribe();
|
||||
}
|
||||
|
||||
onDefaultsSubmit() {
|
||||
this.deviceService.addAccountDefaults(this.defaultsForm).subscribe();
|
||||
}
|
||||
|
||||
onFinished() {
|
||||
window.history.back();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
<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>
|
|
@ -0,0 +1,60 @@
|
|||
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
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
@import '~src/stylesheets/components/buttons';
|
||||
@import '../../../../../../../src/stylesheets/components/buttons';
|
||||
|
||||
.mat-body{
|
||||
margin-bottom: 16px;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue