Merge pull request #3 from MycroftAI/test

Test to dev
pull/6/head
Chris Veilleux 2019-04-02 23:03:42 -05:00 committed by GitHub
commit b2bd8d46b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
285 changed files with 4452 additions and 2729 deletions

29
Jenkinsfile vendored
View File

@ -15,12 +15,13 @@ pipeline {
sh 'ng build --project globalnav' sh 'ng build --project globalnav'
sh 'ng build --project page-not-found' sh 'ng build --project page-not-found'
sh 'ng build --project account --configuration development' sh 'ng build --project account --configuration development'
sh 'ng build --project market --configuration development'
sh 'ng build --project sso --configuration development' sh 'ng build --project sso --configuration development'
} }
} }
// Deploy to the Test environment // Deploy to the Test environment
stage('Build for Test Environment') { stage('Build for Test') {
when { when {
branch 'test' branch 'test'
} }
@ -31,11 +32,12 @@ pipeline {
sh 'ng build --project globalnav' sh 'ng build --project globalnav'
sh 'ng build --project page-not-found' sh 'ng build --project page-not-found'
sh 'ng build --project account --configuration test' sh 'ng build --project account --configuration test'
sh 'ng build --project market --configuration test'
sh 'ng build --project sso --configuration test' sh 'ng build --project sso --configuration test'
} }
} }
stage('Deploy to Test Environment') { stage('Deploy to Test') {
when { when {
branch 'test' branch 'test'
} }
@ -43,22 +45,31 @@ pipeline {
echo 'Deploying to test environment web servers...' echo 'Deploying to test environment web servers...'
withCredentials([sshUserPrivateKey(credentialsId: '6413826d-79f6-4d03-9902-ee1b73a96efd', keyFileVariable: 'JENKINS_SSH_KEY', passphraseVariable: '', usernameVariable: 'SERVER_USER')]) { withCredentials([sshUserPrivateKey(credentialsId: '6413826d-79f6-4d03-9902-ee1b73a96efd', keyFileVariable: 'JENKINS_SSH_KEY', passphraseVariable: '', usernameVariable: 'SERVER_USER')]) {
// Deploy account application and its associated libraries // Deploy account application and its associated libraries
sh 'scp -r dist/shared root@192.81.211.55:/var/www/' echo 'Deploying account application...'
sh 'scp -r dist/globalnav root@192.81.211.55:/var/www/' sh 'scp -r dist/shared root@192.241.152.213:/var/www/'
sh 'scp -r dist/page-not-found root@192.81.211.55:/var/www/' sh 'scp -r dist/globalnav root@192.241.152.213:/var/www/'
sh 'scp -r dist/account root@192.81.211.55:/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 // 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/shared root@198.199.90.118:/var/www/'
sh 'scp -r dist/globalnav 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/page-not-found root@198.199.90.118:/var/www/'
sh 'scp -r dist/sso 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 // Deploy to the Production environment
stage('Build for Production Environment') { stage('Build for Production') {
when { when {
branch 'master' branch 'master'
} }
@ -73,7 +84,7 @@ pipeline {
} }
} }
stage('Deploy to Production Environment') { stage('Deploy to Production') {
when { when {
branch 'master' branch 'master'
} }
@ -81,12 +92,14 @@ pipeline {
echo 'Deploying to production environment web servers...' echo 'Deploying to production environment web servers...'
withCredentials([sshUserPrivateKey(credentialsId: '6413826d-79f6-4d03-9902-ee1b73a96efd', keyFileVariable: 'JENKINS_SSH_KEY', passphraseVariable: '', usernameVariable: 'SERVER_USER')]) { withCredentials([sshUserPrivateKey(credentialsId: '6413826d-79f6-4d03-9902-ee1b73a96efd', keyFileVariable: 'JENKINS_SSH_KEY', passphraseVariable: '', usernameVariable: 'SERVER_USER')]) {
// Deploy account application and its associated libraries // Deploy account application and its associated libraries
echo 'Deploying account application...'
sh 'scp -r dist/shared root@???:/var/www/' sh 'scp -r dist/shared root@???:/var/www/'
sh 'scp -r dist/globalnav 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/page-not-found root@???:/var/www/'
sh 'scp -r dist/account root@???:/var/www/' sh 'scp -r dist/account root@???:/var/www/'
// Deploy single sign on application and its associated libraries // 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/shared root@???:/var/www/'
sh 'scp -r dist/globalnav 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/page-not-found root@???:/var/www/'

View File

@ -87,7 +87,6 @@
"main": "src/test.ts", "main": "src/test.ts",
"polyfills": "src/polyfills.ts", "polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json", "tsConfig": "src/tsconfig.spec.json",
"karmaConfig": "src/karma.conf.js",
"styles": [ "styles": [
"src/styles.scss", "src/styles.scss",
"src/theme.scss" "src/theme.scss"
@ -231,7 +230,15 @@
"with": "projects/market/src/environments/environment.test.ts" "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": { "development": {
"fileReplacements": [ "fileReplacements": [
@ -393,7 +400,8 @@
"namedChunks": false, "namedChunks": false,
"aot": true, "aot": true,
"extractLicenses": true, "extractLicenses": true,
"vendorChunk": false, "vendorChunk": true,
"commonChunk": true,
"buildOptimizer": true, "buildOptimizer": true,
"budgets": [ "budgets": [
{ {
@ -410,7 +418,16 @@
"with": "projects/sso/src/environments/environment.test.ts" "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": { "development": {
"fileReplacements": [ "fileReplacements": [
@ -608,6 +625,7 @@
"aot": true, "aot": true,
"extractLicenses": true, "extractLicenses": true,
"vendorChunk": false, "vendorChunk": false,
"commonChunk": true,
"buildOptimizer": true, "buildOptimizer": true,
"budgets": [ "budgets": [
{ {
@ -623,8 +641,7 @@
"replace": "projects/account/src/environments/environment.ts", "replace": "projects/account/src/environments/environment.ts",
"with": "projects/account/src/environments/environment.dev.ts" "with": "projects/account/src/environments/environment.dev.ts"
} }
], ]
"outputHashing": "all"
}, },
"test": { "test": {
"fileReplacements": [ "fileReplacements": [
@ -633,7 +650,16 @@
"with": "projects/account/src/environments/environment.test.ts" "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
} }
} }
}, },

932
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -35,6 +35,7 @@
"core-js": "^2.5.4", "core-js": "^2.5.4",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"ngx-cookie-service": "^2.1.0", "ngx-cookie-service": "^2.1.0",
"ngx-stripe": "^7.2.0",
"rxjs": "~6.3.3", "rxjs": "~6.3.3",
"zone.js": "~0.8.26" "zone.js": "~0.8.26"
}, },

View File

@ -1,23 +1,26 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { CreateAccountComponent } from './create-account/create-account.component'; import { AccountResolverService } from './core/guards/account-resolver.service';
import { DeviceComponent } from './device/device.component'; import { DashboardComponent } from './modules/dashboard/dashboard.component';
import { PageNotFoundComponent } from 'page-not-found'; import { PageNotFoundComponent } from 'page-not-found';
import { ProfileComponent } from './profile/profile.component';
import { SkillComponent } from './skill/skill.component';
const routes: Routes = [ const routes: Routes = [
{ path: 'create-account', component: CreateAccountComponent }, { path: 'dashboard', component: DashboardComponent, resolve: {account: AccountResolverService} },
{ path: 'device', component: DeviceComponent }, { path: '', redirectTo: '/dashboard', pathMatch: 'full'},
{ path: 'profile', component: ProfileComponent },
{ path: 'skill', component: SkillComponent },
{ path: '', redirectTo: '/profile', pathMatch: 'full' },
{ path: '**', component: PageNotFoundComponent } { path: '**', component: PageNotFoundComponent }
]; ];
@NgModule({ @NgModule({
imports: [ RouterModule.forRoot(routes) ], imports: [
RouterModule.forRoot(
routes,
{
anchorScrolling: 'enabled',
scrollPositionRestoration: 'enabled'
}
)
],
exports: [ RouterModule ] exports: [ RouterModule ]
}) })
export class AppRoutingModule { export class AppRoutingModule {

View File

@ -5,13 +5,13 @@ import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; 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 { GlobalnavModule } from 'globalnav';
import { PageNotFoundModule } from 'page-not-found'; import { PageNotFoundModule } from 'page-not-found';
import { DeviceModule } from './device/device.module'; import { DeviceModule } from './modules/device/device.module';
import { ProfileModule } from './profile/profile.module'; import { ProfileModule } from './modules/profile/profile.module';
import { SharedModule } from 'shared'; import { SharedModule } from 'shared';
import { SkillModule } from './skill/skill.module'; import { SkillModule } from './modules/skill/skill.module';
@NgModule( @NgModule(
{ {
@ -19,7 +19,7 @@ import { SkillModule } from './skill/skill.module';
imports: [ imports: [
BrowserModule, BrowserModule,
BrowserAnimationsModule, BrowserAnimationsModule,
CreateAccountModule, DashboardModule,
GlobalnavModule, GlobalnavModule,
HttpClientModule, HttpClientModule,
DeviceModule, DeviceModule,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,45 +2,12 @@ import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Skill } from '@account/models/skill.model';
import { SkillSettings } from '@account/models/skill-settings.model';
const accountSkillUrl = '/api/skills'; const accountSkillUrl = '/api/skills';
const accountDeviceCountUrl = '/api/device-count'; 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({ @Injectable({
providedIn: 'root' providedIn: 'root'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +0,0 @@
mat-form-field {
margin-bottom: 20px;
width: 100%;
}
fa-icon {
padding-right: 10px
}

View File

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

View File

@ -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">-->
<!--&lt;!&ndash; Radio buttons for user-defined groups &ndash;&gt;-->
<!--<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>-->
<!--&lt;!&ndash; Radio button to add a new user-defined group &ndash;&gt;-->
<!--<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>-->

View File

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

View File

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

View File

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

View File

@ -1,8 +0,0 @@
<account-device-attribute-edit
[dialogData]="data"
[dialogInstructions]="dialogInstructions"
[dialogRef]="dialogRef"
[dialogTitle]="'Group'"
[possibleValues]="deviceGroups"
>
</account-device-attribute-edit>

View File

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

View File

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

View File

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

View File

@ -1,8 +0,0 @@
<account-device-attribute-edit
[dialogData]="data"
[dialogInstructions]="dialogInstructions"
[dialogRef]="dialogRef"
[dialogTitle]="'Placement'"
[possibleValues]="devicePlacements"
>
</account-device-attribute-edit>

View File

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

View File

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

View File

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

View File

@ -1,8 +0,0 @@
<account-device-attribute-edit
[dialogData]="data"
[dialogInstructions]="dialogInstructions"
[dialogRef]="dialogRef"
[dialogTitle]="'Voice'"
[possibleValues]="deviceVoices"
>
</account-device-attribute-edit>

View File

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

View File

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

View File

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

View File

@ -1,8 +0,0 @@
<account-device-attribute-edit
[dialogData]="data"
[dialogInstructions]="dialogInstructions"
[dialogRef]="dialogRef"
[dialogTitle]="'Wake Word'"
[possibleValues]="deviceWakeWords"
>
</account-device-attribute-edit>

View File

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

View File

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

View File

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

View File

@ -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}}:&nbsp;&nbsp;</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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
mat-card-content {
margin-top: 32px;
}

View File

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

View File

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

View File

@ -1,77 +1,70 @@
@import '~@angular/material/theming'; @import "~@angular/material/theming";
@import '~src/stylesheets/mycroft-colors'; @import '~src/stylesheets/mycroft-colors';
@import '~src/stylesheets/components/buttons'; @import '~src/stylesheets/components/buttons';
@mixin panel-defaults { @mixin panel-defaults {
border-radius: 12px; border-radius: 12px;
max-width: 1000px; width: 330px;
min-width: 250px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
margin-bottom: 16px;
} }
#add-device-button { #add-device-button {
@include action-button-primary; @include action-button-primary;
@include panel-defaults; @include panel-defaults;
border-radius: 12px;
cursor: pointer; cursor: pointer;
height: 50px; height: 50px;
margin-top: 32px; margin-top: 32px;
img {
height: 32px;
margin-left: 16px;
}
.mat-h2 {
margin-bottom: 0;
}
fa-icon { fa-icon {
color: mat-color($mycroft-accent, 'A200'); color: mat-color($mycroft-accent, 'A200');
margin-right: 16px; margin-right: 16px;
} }
}
mat-expansion-panel {
@include panel-defaults;
margin-top: 16px;
button {
width: 100%;
}
fa-icon {
padding-right: 10px
}
img { img {
height: 60px; height: 32px;
} margin-left: 16px;
margin-right: 16px;
mat-form-field {
margin-bottom: 20px;
width: 100%;
}
//.delete-button {
// margin-top: 20px;
//}
.mat-body-primary {
margin-bottom: 0;
} }
.mat-h2 { .mat-h2 {
color: mat-color($mycroft-primary);
margin-bottom: 0; margin-bottom: 0;
} }
.mat-subheader { }
padding: 0;
mat-card {
@include panel-defaults;
button {
margin-bottom: 8px;
margin-left: 8px;
margin-right: 8px;
} }
.settings-button { mat-card-title {
margin-bottom: 20px; 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;
}
}
} }

View File

@ -1,10 +1,12 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material'; 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 { DeviceService } from '@account/http/device.service';
import { RemoveComponent } from '../remove/remove.component'; import { Device } from '@account/models/device.model';
import { RemoveComponent } from '../../../remove/remove.component';
@Component({ @Component({
selector: 'account-device-list', selector: 'account-device-list',
@ -13,7 +15,6 @@ import { RemoveComponent } from '../remove/remove.component';
}) })
export class DeviceListComponent implements OnInit { export class DeviceListComponent implements OnInit {
public addIcon = faPlus; public addIcon = faPlus;
public deleteIcon = faTrash;
public devices: Device[]; public devices: Device[];
public platforms = { public platforms = {
'mark-one': {icon: '../assets/mark-1-icon.svg', displayName: 'Mark I'}, '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'} 'kde': {icon: '../assets/kde-icon.svg', displayName: 'KDE'}
}; };
private selectedDevice: Device; 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() { 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}); const removalDialogRef = this.dialog.open(RemoveComponent, {data: false});
this.selectedDevice = device; this.selectedDevice = device;
removalDialogRef.afterClosed().subscribe( 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]; const knownPlatform = this.platforms[device.platform];
return [ return knownPlatform ? knownPlatform.displayName : device.platform;
{name: 'Platform', value: knownPlatform ? knownPlatform.displayName : device.platform},
{name: 'Core Version', value: device.coreVersion},
{name: 'Enclosure Version', value: device.enclosureVersion}
];
} }
getDeviceIcon(device: Device) { getDeviceIcon(device: Device) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
@import '~src/stylesheets/components/buttons'; @import '../../../../../../../src/stylesheets/components/buttons';
.mat-body{ .mat-body{
margin-bottom: 16px; margin-bottom: 16px;

Some files were not shown because too many files have changed in this diff Show More