Moved marketplace UI into the a new angular workspace that will hold all selene angular projects

pull/1/head
Chris Veilleux 2018-10-31 15:42:35 -05:00
commit e3d039e0e6
105 changed files with 13814 additions and 0 deletions

13
.editorconfig Normal file
View File

@ -0,0 +1,13 @@
# Editor configuration, see http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false

39
.gitignore vendored Normal file
View File

@ -0,0 +1,39 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# dependencies
/node_modules
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db

27
README.md Normal file
View File

@ -0,0 +1,27 @@
# Internet
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.0.3.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).

289
angular.json Normal file
View File

@ -0,0 +1,289 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"internet": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"prefix": "app",
"schematics": {
"@schematics/angular:component": {
"styleext": "scss"
}
},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/internet",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "internet:build"
},
"configurations": {
"production": {
"browserTarget": "internet:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "internet:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json",
"karmaConfig": "src/karma.conf.js",
"styles": [
"src/styles.scss"
],
"scripts": [],
"assets": [
"src/favicon.ico",
"src/assets"
]
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
},
"internet-e2e": {
"root": "e2e/",
"projectType": "application",
"prefix": "",
"architect": {
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "internet:serve"
},
"configurations": {
"production": {
"devServerTarget": "internet:serve:production"
}
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": "e2e/tsconfig.e2e.json",
"exclude": [
"**/node_modules/**"
]
}
}
}
},
"market": {
"root": "projects/market/",
"sourceRoot": "projects/market/src",
"projectType": "application",
"prefix": "market",
"schematics": {
"@schematics/angular:component": {
"styleext": "scss",
"spec": false
},
"@schematics/angular:class": {
"spec": false
},
"@schematics/angular:directive": {
"spec": false
},
"@schematics/angular:guard": {
"spec": false
},
"@schematics/angular:module": {
"spec": false
},
"@schematics/angular:pipe": {
"spec": false
},
"@schematics/angular:service": {
"spec": false
}
},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/market",
"index": "projects/market/src/index.html",
"main": "projects/market/src/main.ts",
"polyfills": "projects/market/src/polyfills.ts",
"tsConfig": "projects/market/tsconfig.app.json",
"assets": [
"projects/market/src/favicon.ico",
"projects/market/src/assets"
],
"styles": [
"projects/market/src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "projects/market/src/environments/environment.ts",
"with": "projects/market/src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "market:build"
},
"configurations": {
"production": {
"browserTarget": "market:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "market:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "projects/market/src/test.ts",
"polyfills": "projects/market/src/polyfills.ts",
"tsConfig": "projects/market/tsconfig.spec.json",
"karmaConfig": "projects/market/karma.conf.js",
"styles": [
"projects/market/src/styles.scss"
],
"scripts": [],
"assets": [
"projects/market/src/favicon.ico",
"projects/market/src/assets"
]
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"projects/market/tsconfig.app.json",
"projects/market/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
},
"market-e2e": {
"root": "projects/market-e2e/",
"projectType": "application",
"prefix": "",
"architect": {
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "projects/market-e2e/protractor.conf.js",
"devServerTarget": "market:serve"
},
"configurations": {
"production": {
"devServerTarget": "market:serve:production"
}
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": "projects/market-e2e/tsconfig.e2e.json",
"exclude": [
"**/node_modules/**"
]
}
}
}
}
},
"defaultProject": "internet"
}

10599
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

56
package.json Normal file
View File

@ -0,0 +1,56 @@
{
"name": "internet",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "~7.0.0",
"@angular/cdk": "^7.0.1",
"@angular/common": "~7.0.0",
"@angular/compiler": "~7.0.0",
"@angular/core": "~7.0.0",
"@angular/flex-layout": "^7.0.0-beta.19",
"@angular/forms": "~7.0.0",
"@angular/http": "~7.0.0",
"@angular/material": "^7.0.1",
"@angular/platform-browser": "~7.0.0",
"@angular/platform-browser-dynamic": "~7.0.0",
"@angular/router": "~7.0.0",
"@fortawesome/angular-fontawesome": "^0.3.0",
"@fortawesome/fontawesome-svg-core": "^1.2.7",
"@fortawesome/free-solid-svg-icons": "^5.4.2",
"angular-font-awesome": "^3.1.2",
"core-js": "^2.5.4",
"font-awesome": "^4.7.0",
"rxjs": "~6.3.3",
"zone.js": "~0.8.26"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.10.0",
"@angular/cli": "~7.0.3",
"@angular/compiler-cli": "~7.0.0",
"@angular/language-service": "~7.0.0",
"@types/node": "~8.9.4",
"@types/jasmine": "~2.8.8",
"@types/jasminewd2": "~2.0.3",
"codelyzer": "~4.5.0",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~3.0.0",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~1.1.2",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tslint": "~5.11.0",
"typescript": "~3.1.1"
}
}

View File

@ -0,0 +1,28 @@
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require('jasmine-spec-reporter');
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.e2e.json')
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};

View File

@ -0,0 +1,14 @@
import { AppPage } from './app.po';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getParagraphText()).toEqual('Welcome to market!');
});
});

View File

@ -0,0 +1,11 @@
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo() {
return browser.get('/');
}
getParagraphText() {
return element(by.css('market-root h1')).getText();
}
}

View File

@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/app",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

View File

@ -0,0 +1,2 @@
dist
node_modules

View File

@ -0,0 +1,14 @@
# Multistage Dockerfile to build the marketplace UI and a web server to run it
# STAGE ONE: build the marketplace angular application
FROM node:latest as build
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
ARG selene_env
RUN npm run build-${selene_env}
# STAGE TWO: build the web server and copy the compiled angular app to it.
FROM nginx:latest
COPY --from=build /usr/src/app/dist/mycroft-marketplace /usr/share/nginx/html

View File

@ -0,0 +1,11 @@
# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
#
# For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed
> 0.5%
last 2 versions
Firefox ESR
not dead
not IE 9-11

View File

@ -0,0 +1,31 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../../coverage'),
reports: ['html', 'lcovonly'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false
});
};

View File

@ -0,0 +1,8 @@
{
"/api/*": {
"target": "http://localhost:5002",
"secure": false,
"logLevel": "debug",
"changeOrigin": true
}
}

View File

@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
const routes: Routes = [
{ path: '', redirectTo: '/skills', pathMatch: 'full' },
{ path: '**', component: PageNotFoundComponent }
];
@NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ]
})
export class AppRoutingModule {
}

View File

@ -0,0 +1,4 @@
<market-header></market-header>
<div class="app-body">
<router-outlet></router-outlet>
</div>

View File

@ -0,0 +1,7 @@
@import '../stylesheets/global';
.app-body {
margin-left: 3vw;
margin-right: 3vw;
margin-top: 30px;
}

View File

@ -0,0 +1,11 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'market-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
constructor() { }
ngOnInit() { }
}

View File

@ -0,0 +1,30 @@
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HeaderModule } from './header/header.module';
import { MaterialModule } from './shared/material.module';
import { LoginService } from './shared/login.service';
import { SkillsModule } from './skills/skills.module';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
@NgModule(
{
declarations: [ AppComponent, PageNotFoundComponent ],
imports: [
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
HeaderModule,
MaterialModule,
SkillsModule,
AppRoutingModule
],
providers: [ LoginService ],
bootstrap: [ AppComponent ]
}
)
export class AppModule { }

View File

@ -0,0 +1,22 @@
<mat-toolbar>
<img src="../../assets/header-logo.svg">
<fa-icon class='separator' [icon]="separatorIcon"></fa-icon>
<div class="mat-subheading-1" style="margin-bottom: 0">MARKETPLACE</div>
<div fxFlex fxLayout="row" fxLayoutAlign="end center">
<button mat-button (click)="login()" *ngIf="!isLoggedIn">
<fa-icon [icon]="signInIcon"></fa-icon>
LOG IN
</button>
<button mat-button class="menu-button" [matMenuTriggerFor]="menu" *ngIf="isLoggedIn">
{{userMenuButtonText}}
<fa-icon [icon]="menuButtonIcon"></fa-icon>
</button>
<mat-menu [overlapTrigger]="false" #menu="matMenu">
<button mat-menu-item (click)="logout()">
<fa-icon [icon]="signOutIcon"></fa-icon>
Logout
</button>
</mat-menu>
</div>
</mat-toolbar>

View File

@ -0,0 +1,26 @@
@import '../../stylesheets/global';
mat-toolbar {
background-color: $mycroft-primary;
color: $mycroft-white;
img {
height: 20px;
margin-top: -7px;
}
.separator {
font-size: 5px;
padding-left: 10px;
padding-right: 10px;
}
.mat-subheading-1 {
margin-bottom: 0;
}
fa-icon {
padding-right: 5px;
}
.menu-button {
fa-icon {
padding-left: 5px;
}
}
}

View File

@ -0,0 +1,60 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/internal/Subscription';
import {
faCaretDown,
faCircle,
faSignInAlt,
faSignOutAlt
} from '@fortawesome/free-solid-svg-icons';
import { InstallService } from '../skills/install.service';
import { LoginService } from '../shared/login.service';
@Component({
selector: 'market-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss']
})
export class HeaderComponent implements OnInit, OnDestroy {
public isLoggedIn: boolean;
private loginStatus: Subscription;
public separatorIcon = faCircle;
public signInIcon = faSignInAlt;
public signOutIcon = faSignOutAlt;
public menuButtonIcon = faCaretDown;
public userMenuButtonText: string;
constructor(
private installService: InstallService,
private loginService: LoginService
) { }
ngOnInit() {
this.loginStatus = this.loginService.isLoggedIn.subscribe(
(isLoggedIn) => { this.onLoginStateChange(isLoggedIn); }
);
this.loginService.setLoginStatus();
}
ngOnDestroy() {
this.loginStatus.unsubscribe();
}
onLoginStateChange(isLoggedIn) {
this.isLoggedIn = isLoggedIn;
if (isLoggedIn) {
this.loginService.getUser().subscribe(
(user) => { this.userMenuButtonText = user.name; }
);
}
}
login() {
this.loginService.login();
}
logout() {
this.loginService.logout();
}
}

View File

@ -0,0 +1,22 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FlexLayoutModule } from '@angular/flex-layout';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { InstallService } from '../skills/install.service';
import { HeaderComponent } from './header.component';
import { MaterialModule } from '../shared/material.module';
@NgModule({
imports: [
CommonModule,
FlexLayoutModule,
FontAwesomeModule,
MaterialModule
],
declarations: [ HeaderComponent],
exports: [ HeaderComponent ],
providers: [ InstallService ]
})
export class HeaderModule { }

View File

@ -0,0 +1 @@
<h2>Page not found</h2>

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'market-page-not-found',
templateUrl: './page-not-found.component.html',
styleUrls: ['./page-not-found.component.scss']
})
export class PageNotFoundComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}

View File

@ -0,0 +1,15 @@
import { TestBed, inject } from '@angular/core/testing';
import { LoginService } from './login.service';
describe('LoginService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [LoginService]
});
});
it('should be created', inject([LoginService], (service: LoginService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,41 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { Subject } from 'rxjs/internal/Subject';
import { environment } from '../../environments/environment';
const redirectQuery = '?redirect=';
export class User {
name: string;
}
@Injectable()
export class LoginService {
public isLoggedIn = new Subject<boolean>();
public loginUrl: string = environment.loginUrl + '/login';
private logoutUrl = environment.loginUrl + '/logout';
private userUrl = '/api/user';
constructor(private http: HttpClient) {
}
getUser(): Observable<User> {
return this.http.get<User>(this.userUrl);
}
setLoginStatus(): void {
const cookies = document.cookie;
const seleneTokenExists = cookies.includes('seleneToken');
const seleneTokenEmpty = cookies.includes('seleneToken=""');
this.isLoggedIn.next( seleneTokenExists && !seleneTokenEmpty);
}
login(): void {
window.location.assign(this.loginUrl + redirectQuery + window.location.href);
}
logout(): void {
window.location.assign(this.logoutUrl + redirectQuery + window.location.href);
}
}

View File

@ -0,0 +1,48 @@
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDialogModule } from '@angular/material/dialog';
import { MatDividerModule} from '@angular/material/divider';
import { MatFormFieldModule} from '@angular/material/form-field';
import { MatInputModule} from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip';
@NgModule(
{
imports: [
MatButtonModule,
MatCardModule,
MatDialogModule,
MatDividerModule,
MatFormFieldModule,
MatFormFieldModule,
MatMenuModule,
MatProgressSpinnerModule,
MatSelectModule,
MatSnackBarModule,
MatToolbarModule,
MatTooltipModule
],
exports: [
MatButtonModule,
MatCardModule,
MatDialogModule,
MatDividerModule,
MatFormFieldModule,
MatInputModule,
MatMenuModule,
MatProgressSpinnerModule,
MatSelectModule,
MatSnackBarModule,
MatToolbarModule,
MatTooltipModule
]
}
)
export class MaterialModule { }

View File

@ -0,0 +1,71 @@
<ng-container [ngSwitch]="installStatus">
<!-- Give the user the ability to remove an installed skill -->
<button
*ngSwitchCase="'installed'"
fxLayout="row"
fxLayoutAlign="center center"
mat-flat-button
class="uninstall-button"
(click)="uninstallSkill()"
[ngStyle]="installButtonStyle"
>
<fa-icon [icon]="removeIcon"></fa-icon>
<span>REMOVE</span>
</button>
<!-- System skills cannot be removed so show a lock icon and disable -->
<button
*ngSwitchCase="'system'"
fxLayout="row"
fxLayoutAlign="center center"
mat-flat-button
class="installed-button"
[disabled]="true"
[ngStyle]="installButtonStyle"
>
<fa-icon [icon]="skillLocked"></fa-icon>
<span>ADDED</span>
</button>
<!-- Use the button to indicate to the user that the install is in progress -->
<button
*ngSwitchCase="'installing'"
fxLayout="row"
fxLayoutAlign="center center"
mat-flat-button
class="installing-button"
[ngStyle]="installButtonStyle"
>
<div class="installing-spinner"></div>
ADDING...
</button>
<!-- Use the button to indicate to the user that an uninstall is in progress -->
<button
*ngSwitchCase="'uninstalling'"
fxLayout="row"
fxLayoutAlign="center center"
mat-flat-button
class="uninstalling-button"
[ngStyle]="installButtonStyle"
>
<div class="uninstalling-spinner"></div>
REMOVING...
</button>
<!-- Allow the user to add a skill to their devices -->
<button
*ngSwitchDefault
fxLayout="row"
fxLayoutAlign="center center"
class="install-button"
mat-flat-button
(click)="install_skill()"
[ngStyle]="installButtonStyle"
>
<fa-icon [icon]="addIcon"></fa-icon>
<span>ADD</span>
</button>
</ng-container>

View File

@ -0,0 +1,80 @@
@import '../../../stylesheets/global.scss';
@mixin install-status {
border-radius: 4px;
letter-spacing: 0.5px;
}
// The angular material spinner was limiting in color choices we built our own
@mixin spinner-common {
animation: spin 1s ease-in-out infinite;
border: 2px solid rgba(255,255,255,.3);
border-radius: 50%;
display: inline-block;
height: 15px;
margin-right: 10px;
width: 15px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
fa-icon {
margin-right: 10px;
opacity: 0.6;
}
.install-button {
@include install-status;
background-color: $mycroft-primary ;
color: $mycroft-white;
}
.install-button:hover {
@include install-status;
background-color: $mycroft-tertiary-green;
color: $mycroft-secondary;
}
.installed-button {
@include install-status;
}
.installing-button {
@include install-status;
background-color: $mycroft-tertiary-green;
color: $mycroft-secondary;
mat-spinner {
float: left;
margin-right: 10px;
margin-top: 7px;
}
}
.installing-spinner {
@include spinner-common;
border-right-color: $mycroft-secondary;
border-top-color: $mycroft-secondary;
}
.uninstall-button {
@include install-status;
background-color: $mycroft-dark-grey;
color: $mycroft-white;
}
.uninstall-button:hover {
@include install-status;
border: none;
background-color: #eb5757;
color: $mycroft-white;
}
.uninstalling-button {
@include install-status;
background-color: #eb5757;
color: $mycroft-white;
}
.uninstalling-spinner {
@include spinner-common;
border-right-color: $mycroft-white;
border-top-color: $mycroft-white;
}

View File

@ -0,0 +1,146 @@
/**
* This component does all things install button, which is a lot of things.
*/
import { Component, Input, OnInit } from '@angular/core';
import { AvailableSkill } from '../skills.service';
import { InstallService } from '../install.service';
import { faPlusCircle } from '@fortawesome/free-solid-svg-icons/faPlusCircle';
import { faTrash } from '@fortawesome/free-solid-svg-icons/faTrash';
import { faLock } from '@fortawesome/free-solid-svg-icons/faLock';
import { MatSnackBar } from '@angular/material';
const fiveSeconds = 5000;
const tenSeconds = 10000;
@Component({
selector: 'market-skill-install-button',
templateUrl: './install-button.component.html',
styleUrls: ['./install-button.component.scss']
})
export class InstallButtonComponent implements OnInit {
public addIcon = faPlusCircle;
@Input() private component: string;
public installButtonStyle: object;
public installStatus: string;
public removeIcon = faTrash;
@Input() public skill: AvailableSkill;
public skillLocked = faLock;
constructor(private installSnackbar: MatSnackBar, private installService: InstallService) { }
ngOnInit() {
this.installService.installStatuses.subscribe(
(installStatuses) => {
this.installStatus = this.installService.getSkillInstallStatus(
this.skill.name,
this.skill.isSystemSkill,
installStatuses
);
}
);
this.applyInstallButtonStyle();
}
/**
* Some of the install button style elements are different depending on
* which component it is displayed within. Use the ngStyle directive
* to specify these styles.
*/
applyInstallButtonStyle() {
if (this.component === 'skillDetail') {
this.installButtonStyle = {'width': '140px'};
} else if (this.component === 'skillSummary') {
this.installButtonStyle = {'width': '320px', 'margin-bottom': '15px'};
}
}
/**
* Install a skill onto one or many devices
*/
install_skill(): void {
this.installService.addToInstallQueue(this.skill.name).subscribe(
(response) => {
this.onInstallSuccess(response);
},
(response) => {
this.onInstallFailure(response);
}
);
}
/**
* Handle the successful install request
*
* This does not indicate that the install of the skill completed, only
* that the request to install a skill succeeded. Change the install
* button to an 'installing' state.
*
* @param response: response object from the install endpoint
*/
onInstallSuccess(response): void {
this.installService.newInstallStatuses[this.skill.name] = 'installing';
this.installService.applyInstallStatusChanges();
this.installService.checkInstallationsInProgress();
this.installSnackbar.open(
'The ' + this.skill.title + ' skill is being added ' +
'to your devices. Please allow up to two minutes for ' +
'installation to complete before using the skill.',
null,
{panelClass: 'mycroft-snackbar', duration: tenSeconds}
);
}
/**
* Handle the failure to install a skill.
*
* If a user attempts to install a skill without being logged in, show a
* snackbar to notify the user and give them the ability to log in.
*
* @param response - object representing the response from the API call
*/
onInstallFailure(response): void {
if (response.status === 401) {
this.installSnackbar.open(
'To install a skill, log in to your account.',
'LOG IN',
{panelClass: 'mycroft-snackbar', duration: fiveSeconds}
);
}
}
/**
* Remove a skill from one or many devices
*/
uninstallSkill(): void {
this.installService.addToUninstallQueue(this.skill.name).subscribe(
(response) => {
this.onUninstallSuccess(response);
},
);
}
/**
* Handle the successful install request
*
* This does not indicate that the install of the skill completed, only
* that the request to install a skill succeeded. Change the install
* button to an 'installing' state.
*
* @param response - object representing the response from the API call
*/
onUninstallSuccess(response): void {
this.installService.newInstallStatuses[this.skill.name] = 'uninstalling';
this.installService.applyInstallStatusChanges();
this.installService.checkInstallationsInProgress();
this.installSnackbar.open(
'The ' + this.skill.title + ' skill is ' +
'uninstalling. Please allow up to a minute for the skill to be ' +
'removed from devices.',
null,
{panelClass: 'mycroft-snackbar', duration: tenSeconds}
);
}
}

View File

@ -0,0 +1,219 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { AvailableSkill, SkillDetail } from './skills.service';
// Status values that can be expected in the install status endpoint response.
type InstallStatus = 'failed' | 'installed' | 'installing' | 'uninstalling';
export interface SkillInstallStatus {
[key: string]: InstallStatus;
}
export interface FailureReason {
[key: string]: string;
}
export interface Installations {
failureReasons: FailureReason;
installStatuses: SkillInstallStatus;
}
const inProgressStatuses = ['installing', 'uninstalling', 'failed'];
const installStatusUrl = '/api/skill/installations';
const installerSettingsUrl = '/api/skill/install';
@Injectable({
providedIn: 'root'
})
export class InstallService {
public failureReasons: FailureReason;
public installStatuses = new Subject<SkillInstallStatus>();
public newInstallStatuses: SkillInstallStatus;
private prevInstallStatuses: SkillInstallStatus;
public statusNotifications = new Subject<string[]>();
constructor(private http: HttpClient) { }
/** Issue API call to get the current state of skill installations */
getSkillInstallations() {
this.http.get<Installations>(installStatusUrl).subscribe(
(installations) => {
this.newInstallStatuses = installations.installStatuses;
this.failureReasons = installations.failureReasons;
this.applyInstallStatusChanges();
this.checkInstallationsInProgress();
}
);
}
/** Emit changes to install statuses */
applyInstallStatusChanges() {
if (this.prevInstallStatuses) {
Object.keys(this.prevInstallStatuses).forEach(
(skillName) => { this.compareStatuses(skillName); }
);
}
this.prevInstallStatuses = this.newInstallStatuses;
this.installStatuses.next(this.newInstallStatuses);
}
/** Compare the new status to the previous status looking for changes
*
* There is a race condition where the skill status on the device may not
* change between the time a user clicks a button in the marketplace and
* the next call of the status endpoint.
*
* For example, there is a period of time between the install button
* on the marketplace being clicked and device(s) retrieving that request.
* If the skill status endpoint is called within this time frame the status
* on the response object will not be 'installing'. This would result in
* the status reverting to its previous state.
*
* To combat this, we check that skill status changes follow a predefined
* progression before reflecting the status change on the UI.
*/
compareStatuses(skillName: string) {
const prevSkillStatus = this.prevInstallStatuses[skillName];
const newSkillStatus = this.newInstallStatuses[skillName];
switch (prevSkillStatus) {
case ('installing'): {
if (newSkillStatus === 'installed') {
this.statusNotifications.next([skillName, newSkillStatus]);
this.removeFromInstallQueue(skillName).subscribe();
} else if (newSkillStatus === 'failed') {
this.statusNotifications.next([skillName, 'install failed']);
} else {
this.newInstallStatuses[skillName] = prevSkillStatus;
}
break;
}
case ('uninstalling'): {
if (!newSkillStatus) {
this.statusNotifications.next([skillName, 'uninstalled']);
this.removeFromUninstallQueue(skillName).subscribe();
} else if (newSkillStatus === 'failed') {
this.statusNotifications.next([skillName, 'uninstall failed']);
} else {
this.newInstallStatuses[skillName] = prevSkillStatus;
}
break;
}
case ('failed'): {
if (!newSkillStatus) {
this.statusNotifications.next([skillName, 'uninstalled']);
} else if (newSkillStatus !== 'installed') {
this.statusNotifications.next([skillName, newSkillStatus]);
} else {
this.newInstallStatuses[skillName] = prevSkillStatus;
}
break;
}
}
}
/***
* Return the install status for the specified skill.
*
* System skills are treated differently than installed skills because they
* cannot be removed from the device. This function will make the differentiation.
*
* @param skillName: unique name of skill being installed
* @param isSystemSkill: skill that has a "system" tag
* @param installStatuses: object containing all device skills and their status
*/
getSkillInstallStatus(
skillName: string,
isSystemSkill: boolean,
installStatuses: SkillInstallStatus
) {
let installStatus: string;
if (isSystemSkill) {
installStatus = 'system';
} else {
installStatus = installStatuses[name];
}
return installStatus;
}
/** Poll at an interval to check the status of install/uninstall requests
*
* We want to avoid polling if we don't need it. Only poll if waiting for
* the result of a requested install/uninstall.
*/
checkInstallationsInProgress() {
const inProgress = Object.values(this.newInstallStatuses).filter(
(installStatus) => inProgressStatuses.includes(installStatus)
);
if (inProgress.length > 0) {
setTimeout(() => { this.getSkillInstallations(); }, 10000);
}
}
/**
* Call the API to add a skill to the Installer skill's 'to_install' setting.
*
* @param skillName: the skill being installed
*/
addToInstallQueue(skillName: string): Observable<Object> {
return this.http.put<Object>(
installerSettingsUrl,
{
action: 'add',
section: 'to_install',
skill_name: skillName
}
);
}
/**
* Call the API to add a skill to the Installer skill's 'to_remove' setting.
*
* @param skillName: the skill being removed
*/
addToUninstallQueue(skillName: string): Observable<Object> {
return this.http.put<Object>(
installerSettingsUrl,
{
action: 'add',
section: 'to_remove',
skill_name: skillName
}
);
}
/**
* Call the API to remove a skill to the Installer skill's 'to_install' setting.
*
* @param skillName: the skill being installed
*/
removeFromInstallQueue(skillName: string): Observable<Object> {
return this.http.put<Object>(
installerSettingsUrl,
{
action: 'remove',
section: 'to_install',
skill_name: skillName
}
);
}
/**
* Call the API to remove a skill to the Installer skill's 'to_remove' setting.
*
* @param skillName: the skill being removed
*/
removeFromUninstallQueue(skillName: string): Observable<Object> {
return this.http.put<Object>(
installerSettingsUrl,
{
action: 'remove',
section: 'to_remove',
skill_name: skillName
}
);
}
}

View File

@ -0,0 +1,57 @@
<div class="skill-detail-body" fxLayout="row wrap">
<!-- Left Side -->
<div class="skill-detail-body-left" fxFlex>
<div class="skill-detail-section">
<div class="mat-subheading-1">hey mycroft</div>
<div fxLayout="row wrap">
<div class="mat-body-1 skill-trigger" *ngFor="let trigger of skill.triggers">
<fa-icon [icon]="triggerIcon"></fa-icon>
{{trigger}}
</div>
</div>
</div>
<div class="skill-detail-section">
<div class="mat-subheading-1">description</div>
<div class="mat-body-1" [innerHTML]="skill.description"></div>
</div>
<div class="skill-detail-section">
<div class="mat-subheading-1">credits</div>
<div class="mat-body-1" *ngFor="let credit of skill.credits">
{{credit.name}}
</div>
</div>
</div>
<!-- Right Side -->
<div class="skill-detail-body-right" fxFlex="20">
<div class="skill-detail-section">
<div class="mat-subheading-1">supported devices</div>
<div *ngIf="skill.worksOnMarkOne" class="mat-body-1" fxLayoutAlign="none center">
<img src="../../../../assets/mark-1-icon.svg">
Mark I
</div>
<div *ngIf="skill.worksOnMarkTwo" class="mat-body-1" fxLayoutAlign="none center">
<img src="../../../../assets/mark-2-icon.svg">
Mark II
</div>
<div *ngIf="skill.worksOnPicroft" class="mat-body-1" fxLayoutAlign="none center">
<img src="../../../../assets/picroft-icon.svg">
Picroft
</div>
<div *ngIf="skill.worksOnKDE" class="mat-body-1" fxLayoutAlign="none center">
<img src="../../../../assets/kde.svg" class="kde-icon">
KDE
</div>
</div>
<div class="skill-detail-section">
<div class="mat-subheading-1">supported languages</div>
<div class="mat-body-1">English</div>
</div>
<div class="skill-detail-section">
<div class="mat-subheading-1">category</div>
<div class="mat-body-1">{{skill.categories[0]}}</div>
</div>
</div>
</div>

View File

@ -0,0 +1,46 @@
@import '../../../../stylesheets/global';
.skill-detail-body {
background-color: $mycroft-white;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
margin-bottom: 50px;
padding-bottom: 3vh;
padding-left: 4vw;
padding-right: 4vw;
padding-top: 3vh;
.mat-subheading-1 {
color: $mycroft-dark-grey;
font-variant: small-caps;
font-weight: 500;
margin-bottom: 5px;
}
.mat-body-1 {
color: $mycroft-secondary;
}
.kde-icon {
height: 40px;
width: 40px;
}
.skill-detail-section {
margin-bottom: 30px;
}
.skill-detail-body-left {
min-width: 340px;
margin-right: 50px;
.skill-trigger {
@include skill-trigger;
@include ellipsis-overflow;
margin-right: 10px;
margin-bottom: 10px;
max-width: 340px;
}
}
.skill-detail-body-right {
margin-right: 20px;
white-space: nowrap;
img {
padding-right: 10px;
}
}
}

View File

@ -0,0 +1,18 @@
import { Component, Input } from '@angular/core';
import { faComment } from '@fortawesome/free-solid-svg-icons';
import { SkillDetail } from '../../skills.service';
@Component({
selector: 'market-skill-detail-body',
templateUrl: './skill-detail-body.component.html',
styleUrls: ['./skill-detail-body.component.scss']
})
export class SkillDetailBodyComponent {
@Input() public skill: SkillDetail;
public triggerIcon = faComment;
constructor() { }
}

View File

@ -0,0 +1,37 @@
<!-- Header block -->
<div class="skill-detail-header" fxLayout="row wrap">
<!-- Left Side -->
<div class="skill-detail-header-left" fxFlex>
<!-- there cannot be an icon and an icon_image. show the
image if it exists otherwise show the icon -->
<img *ngIf="skill.iconImage" src={{skill.iconImage}} height="70" width="70">
<fa
*ngIf="!skill.iconImage"
[ngStyle]="{'color': skill.icon.color}"
name={{skill.icon.icon}}
>
</fa>
<div fxFlex>
<h1>{{skill.title}}</h1>
<div class="mat-body-1" [innerHTML]="skill.summary"></div>
</div>
</div>
<!-- Right Side -->
<div class="skill-detail-header-right" fxFlex="20">
<market-skill-install-button [skill]="skill" [component]="'skillDetail'">
</market-skill-install-button>
<div>
<button
mat-icon-button
class="github-button"
(click)="navigateToGithubRepo(skill.repositoryUrl)"
>
<fa-icon [icon]="githubIcon"></fa-icon>
GitHub Repository
</button>
</div>
</div>
</div>

View File

@ -0,0 +1,48 @@
@import '../../../../stylesheets/global';
.skill-detail-header {
background-color: #f7f9fa;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
padding-bottom: 3vh;
padding-left: 4vw;
padding-right: 4vw;
padding-top: 4vh;
.skill-detail-header-left {
color: $mycroft-secondary;
margin-right: 50px;
min-width: 340px;
fa {
font-size: 70px;
margin-right: 20px;
}
img {
margin-right: 20px;
}
h1 {
font-family: 'Roboto Mono', monospace;
margin-bottom: 10px;
margin-top: 0;
}
}
.skill-detail-header-right {
margin-right: 20px;
.install-button {
@include action-button;
width: 140px;
}
.install-button:hover {
background-color: $mycroft-tertiary-green;
color: $mycroft-secondary;
}
.github-button {
color: $mycroft-dark-grey;
font-weight: normal;
width: 135px;
fa-icon {
padding-right: 5px;
}
}
}
}

View File

@ -0,0 +1,19 @@
import { Component, Input } from '@angular/core';
import { SkillDetail } from '../../skills.service';
import { faCodeBranch } from '@fortawesome/free-solid-svg-icons';
@Component({
selector: 'market-skill-detail-header',
templateUrl: './skill-detail-header.component.html',
styleUrls: ['./skill-detail-header.component.scss']
})
export class SkillDetailHeaderComponent {
public githubIcon = faCodeBranch;
@Input() public skill: SkillDetail;
constructor() { }
navigateToGithubRepo(githubRepoUrl) {
window.open(githubRepoUrl);
}
}

View File

@ -0,0 +1,10 @@
<div class="navigate-back">
<button mat-icon-button [routerLink]="['/skills']">
<fa-icon [icon]="backArrow"></fa-icon>
Back to Skill Listing
</button>
</div>
<div class="skill-detail" *ngIf="skill$ | async as skill">
<market-skill-detail-header [skill]="skill"></market-skill-detail-header>
<market-skill-detail-body [skill]="skill"></market-skill-detail-body>
</div>

View File

@ -0,0 +1,18 @@
@import '../../../stylesheets/global';
@mixin skill-detail-size {
margin: 0 auto;
max-width: 1000px;
}
.navigate-back {
@include skill-detail-size;
color: $mycroft-dark-grey;
padding-bottom: 10px;
}
.skill-detail {
@include skill-detail-size;
box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.12);
border-radius: 10px;
}

View File

@ -0,0 +1,39 @@
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
import { Observable } from 'rxjs/internal/Observable';
import { switchMap, tap } from 'rxjs/operators';
import { faArrowLeft } from '@fortawesome/free-solid-svg-icons';
import { InstallService } from '../install.service';
import { SkillDetail, SkillsService } from '../skills.service';
@Component({
selector: 'market-skill-detail',
templateUrl: './skill-detail.component.html',
styleUrls: ['./skill-detail.component.scss']
})
export class SkillDetailComponent implements OnInit {
public backArrow = faArrowLeft;
public skill$: Observable<SkillDetail>;
constructor(
private installService: InstallService,
private route: ActivatedRoute,
private router: Router,
private skillsService: SkillsService
) { }
ngOnInit() {
this.installService.getSkillInstallations();
this.skill$ = this.route.paramMap.pipe(
switchMap(
(params: ParamMap) => this.skillsService.getSkillById(params.get('skillName'))
),
tap(
() => { this.installService.getSkillInstallations(); }
)
);
}
}

View File

@ -0,0 +1,34 @@
<div fxLayout="row" fxLayoutAlign="space-between start" class="card-header">
<!-- Show a Mycroft icon in the upper left corner of the skill card
if the skill is written by the Mycroft staff.
-->
<div class="mycroft-icon">
<img
src="../../../../assets/mycroft-logo.svg"
*ngIf="skill.isMycroftMade"
matTooltip="Mycroft Made"
>
</div>
<!-- Show the icon corresponding to the skill on the card -->
<div class="skill-icon">
<fa
*ngIf="!skill.iconImage"
[ngStyle]="{'color': skill.icon.color}"
name={{skill.icon.icon}} size="2x"
>
</fa>
<img *ngIf="skill.iconImage" src={{skill.iconImage}} height="30" width="30">
</div>
<!-- Show a check mark icon in the upper right corner when skill is installed -->
<div class="installed-icon">
<fa-icon
*ngIf="isInstalled"
[icon]="installedIcon"
matTooltip="Skill Installed"
>
</fa-icon>
</div>
</div>

View File

@ -0,0 +1,20 @@
@import '../../../../stylesheets/global.scss';
.card-header {
margin-bottom: 20px;
}
.mycroft-icon {
width: 20px;
img {
height: 20px;
width: 20px;
}
}
.installed-icon {
width: 20px;
fa-icon {
color: $mycroft-tertiary-green;
font-size: 20px;
}
}

View File

@ -0,0 +1,37 @@
/**
* Format the header portion of a skill summary card. This includes the icon
* for the skill and a Mycroft logo if the skill is authored by Mycroft AI.
*/
import { Component, Input, OnInit } from '@angular/core';
import { AvailableSkill } from '../../skills.service';
import { InstallService } from '../../install.service';
import { faCheckCircle } from '@fortawesome/free-solid-svg-icons';
@Component({
selector: 'market-skill-card-header',
templateUrl: './skill-card-header.component.html',
styleUrls: ['./skill-card-header.component.scss']
})
export class SkillCardHeaderComponent implements OnInit {
public isInstalled: boolean;
public installedIcon = faCheckCircle;
@Input() public skill: AvailableSkill;
constructor(private installService: InstallService) { }
/**
* Include the Mycroft AI logo in the card header if Mycroft authored the skill
*/
ngOnInit() {
this.installService.installStatuses.subscribe(
(installStatuses) => {
const installStatus = this.installService.getSkillInstallStatus(
this.skill.name,
this.skill.isSystemSkill,
installStatuses
);
this.isInstalled = ['system', 'installed'].includes(installStatus);
}
);
}
}

View File

@ -0,0 +1,22 @@
<!-- This represents a single skill on the skill summary page -->
<mat-card>
<!-- Make the entire card a clickable button that routes to the details -->
<div [routerLink]="['/skill', skill.name]">
<market-skill-card-header [skill]="skill"></market-skill-card-header>
<mat-card-title align="center">
{{skill.title ? skill.title : '&nbsp;'}}
</mat-card-title>
<mat-card-subtitle fxLayoutAlign="center">
<div class="skill-trigger">
<fa-icon [icon]="voiceIcon"></fa-icon>
{{skill.trigger}}
</div>
</mat-card-subtitle>
<mat-card-content [innerHTML]="skill.summary ? skill.summary : '&nbsp;'">
</mat-card-content>
</div>
<mat-card-actions>
<market-skill-install-button [skill]="skill" [component]="'skillSummary'">
</market-skill-install-button>
</mat-card-actions>
</mat-card>

View File

@ -0,0 +1,44 @@
@import '../../../../stylesheets/global.scss';
@mixin card-width {
width: 320px;
}
mat-card {
@include card-width;
border-radius: 10px;
cursor: pointer;
margin: 10px;
padding: 18px;
mat-card-title {
@include ellipsis-overflow;
color: $mycroft-secondary;
font-family: 'Roboto Mono', monospace;
font-weight: bold;
padding-bottom: 5px;
text-align: center;
}
mat-card-subtitle {
.skill-trigger {
@include ellipsis-overflow;
@include skill-trigger;
}
}
mat-card-content {
color: $mycroft-secondary;
@include ellipsis-overflow;
text-align: center;
}
mat-card-actions {
margin-left: 0;
margin-bottom: 0;
}
}
mat-card:hover{
box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.2);
}
.login-snackbar {
text-align: center;
}

View File

@ -0,0 +1,79 @@
/**
* Format the header portion of a skill summary card. This includes the icon
* for the skill and a Mycroft logo if the skill is authored by Mycroft AI.
*/
import { Component, Input, OnInit} from '@angular/core';
import { MatSnackBar } from '@angular/material';
import { faComment } from '@fortawesome/free-solid-svg-icons';
import { AvailableSkill } from '../../skills.service';
import { InstallService } from '../../install.service';
const fiveSeconds = 5000;
@Component({
selector: 'market-skill-card',
templateUrl: './skill-card.component.html',
styleUrls: ['./skill-card.component.scss']
})
export class SkillCardComponent implements OnInit {
@Input() public skill: AvailableSkill;
public voiceIcon = faComment;
constructor(
public installSnackbar: MatSnackBar,
private installService: InstallService) {
}
ngOnInit() {
this.installService.statusNotifications.subscribe(
(statusChange) => {
this.showStatusNotifications(statusChange);
}
);
}
showStatusNotifications(statusChange: string[]) {
let notificationMessage: string;
const [skillName, notificationStatus] = statusChange;
if (this.skill.name === skillName) {
switch (notificationStatus) {
case ('installed'): {
notificationMessage = 'The ' + this.skill.title + ' skill has ' +
'been added to all your devices.';
this.showInstallStatusNotification(notificationMessage);
break;
}
case ('uninstalled'): {
notificationMessage = 'The ' + this.skill.title + ' skill has ' +
'been removed from all your devices.';
this.showInstallStatusNotification(notificationMessage);
break;
}
case ('install failed'): {
notificationMessage = 'The ' + this.skill.title + ' failed to ' +
'install to one or more of your devices. Install will be ' +
'retried until successful';
this.showInstallStatusNotification(notificationMessage);
break;
}
case ('uninstall failed'): {
notificationMessage = 'The ' + this.skill.title + ' failed to ' +
'uninstall from one or more of your devices. Uninstall ' +
'will be retried until successful';
this.showInstallStatusNotification(notificationMessage);
}
}
}
}
showInstallStatusNotification(notificationMessage: string) {
this.installSnackbar.open(
notificationMessage,
'',
{panelClass: 'login-snackbar', duration: fiveSeconds}
);
}
}

View File

@ -0,0 +1,19 @@
<!-- Search bar at top of skill listing page -->
<div fxLayout="row" fxLayoutAlign="center" class="skill-toolbar">
<div fxFlex="60" (keydown.enter)="searchSkills()" class="search-field">
<mat-form-field>
<input matInput placeholder="Search Skills" type="text" [(ngModel)]="searchTerm">
<button mat-icon-button matSuffix="">
<fa-icon [icon]="searchIcon" (click)="searchSkills()"></fa-icon>
</button>
</mat-form-field>
</div>
</div>
<!-- Back to all skills button shown when skill listing is filtered -->
<div *ngIf="showBackButton">
<button mat-icon-button class="back-button" (click)="clearSearch()">
<fa-icon [icon]="backArrow"></fa-icon>
All Skills
</button>
</div>

View File

@ -0,0 +1,21 @@
@import '../../../../stylesheets/global';
fa-icon {
color: $mycroft-dark-grey;
}
.skill-toolbar {
margin-left: 15px;
.search-field {
background-color: white;
border-radius: 10px;
color: $mycroft-dark-grey;
min-width: 330px;
padding-left: 20px;
padding-right: 20px;
padding-top: 10px;
mat-form-field {
width: 100%;
}
}
}

View File

@ -0,0 +1,60 @@
import { Component, EventEmitter, OnInit, OnDestroy, Output } from '@angular/core';
import { Subscription } from 'rxjs/internal/Subscription';
import { faArrowLeft, faSearch } from '@fortawesome/free-solid-svg-icons';
import { InstallService } from '../../install.service';
import { SkillsService } from '../../skills.service';
@Component({
selector: 'market-skill-search',
templateUrl: './skill-search.component.html',
styleUrls: ['./skill-search.component.scss']
})
export class SkillSearchComponent implements OnInit, OnDestroy {
public backArrow = faArrowLeft;
public searchIcon = faSearch;
@Output() public searchResults = new EventEmitter();
public searchTerm: string;
public skillsAreFiltered: Subscription;
public showBackButton = false;
constructor(
private installService: InstallService,
private skillsService: SkillsService
) {
}
ngOnInit() {
this.skillsAreFiltered = this.skillsService.isFiltered.subscribe(
(isFiltered) => { this.onFilteredStateChange(isFiltered); }
);
}
ngOnDestroy() {
this.skillsAreFiltered.unsubscribe();
}
/** Clear out the contents of the search bar. */
clearSearch(): void {
this.searchTerm = '';
this.searchSkills();
}
/** Call the skill search API to return skills matching the search criteria. */
searchSkills(): void {
this.skillsService.searchSkills(this.searchTerm).subscribe(
(skills) => {
this.skillsService.availableSkills = skills;
this.skillsService.getSkillCategories();
this.searchResults.emit(skills);
this.installService.getSkillInstallations();
}
);
}
/** Determine whether or not to show the back button. */
onFilteredStateChange (isFiltered) {
this.showBackButton = isFiltered;
}
}

View File

@ -0,0 +1,9 @@
<market-skill-search (searchResults)="showSearchResults($event)"></market-skill-search>
<div class="skill-category" *ngFor="let category of skillCategories">
<mat-toolbar align="center">{{category}}</mat-toolbar>
<div fxLayout="row wrap">
<ng-container *ngFor="let skill of filterSkillsByCategory(category)">
<market-skill-card [skill]="skill"></market-skill-card>
</ng-container>
</div>
</div>

View File

@ -0,0 +1,22 @@
@import '../../../stylesheets/global';
.skill-category {
background-color: $market-background;
mat-toolbar {
background-color: $market-background;
color: $mycroft-dark-grey;
font-size: xx-large;
margin-top: 20px;
padding-left: 10px;
fa-icon {
margin-right: 15px;
}
}
}
.back-button {
color: $mycroft-dark-grey;
margin-left: 20px;
width: 100px;
}

View File

@ -0,0 +1,49 @@
import { Component, OnInit } from '@angular/core';
import { SkillsService, AvailableSkill } from '../skills.service';
import { InstallService } from '../install.service';
@Component({
selector: 'market-skill-summary',
templateUrl: './skill-summary.component.html',
styleUrls: ['./skill-summary.component.scss'],
})
export class SkillSummaryComponent implements OnInit {
public skillCategories: string[];
public availableSkills: AvailableSkill[];
constructor(
private installService: InstallService,
private skillsService: SkillsService,
) { }
ngOnInit() {
this.getAvailableSkills();
}
/** Issue and API call to retrieve all the available skills. */
getAvailableSkills(): void {
this.skillsService.getAvailableSkills().subscribe(
(skills) => {
this.availableSkills = skills;
this.skillCategories = this.skillsService.getSkillCategories();
this.installService.getSkillInstallations();
}
);
}
/** Skills are displayed by category; this function will do the filtering */
filterSkillsByCategory(category: string): AvailableSkill[] {
return this.availableSkills.filter(
(skill) => skill.marketCategory === category
);
}
/** Change the view to display only those matching the search criteria. */
showSearchResults(searchResults): void {
this.availableSkills = searchResults;
this.skillCategories = this.skillsService.getSkillCategories();
}
}

View File

@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { SkillSummaryComponent } from './skill-summary/skill-summary.component';
import { SkillDetailComponent } from './skill-detail/skill-detail.component';
const routes: Routes = [
{ path: 'skills', component: SkillSummaryComponent },
{ path: 'skill/:skillName', component: SkillDetailComponent}
];
@NgModule({
imports: [ RouterModule.forChild(routes) ],
exports: [ RouterModule ]
})
export class SkillsRoutingModule { }

View File

@ -0,0 +1,48 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FlexLayoutModule } from '@angular/flex-layout';
import { FormsModule } from '@angular/forms';
import { AngularFontAwesomeModule } from 'angular-font-awesome';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { InstallButtonComponent } from './install-button/install-button.component';
import { InstallService } from './install.service';
import { MaterialModule } from '../shared/material.module';
import { SkillCardComponent } from './skill-summary/skill-card/skill-card.component';
import { SkillDetailBodyComponent } from './skill-detail/skill-detail-body/skill-detail-body.component';
import { SkillCardHeaderComponent } from './skill-summary/skill-card/skill-card-header.component';
import { SkillDetailComponent } from './skill-detail/skill-detail.component';
import { SkillDetailHeaderComponent } from './skill-detail/skill-detail-header/skill-detail-header.component';
import { SkillSearchComponent} from './skill-summary/skill-search/skill-search.component';
import { SkillsRoutingModule } from './skills-routing.module';
import { SkillsService } from './skills.service';
import { SkillSummaryComponent } from './skill-summary/skill-summary.component';
@NgModule(
{
imports: [
AngularFontAwesomeModule,
CommonModule,
FlexLayoutModule,
FontAwesomeModule,
FormsModule,
MaterialModule,
SkillsRoutingModule
],
declarations: [
SkillCardComponent,
SkillCardHeaderComponent,
SkillDetailComponent,
SkillDetailBodyComponent,
SkillDetailHeaderComponent,
SkillSearchComponent,
SkillSummaryComponent,
InstallButtonComponent
],
exports: [ SkillSummaryComponent, SkillDetailComponent ],
entryComponents: [ SkillDetailComponent ],
providers: [ InstallService, SkillsService ]
}
)
export class SkillsModule { }

View File

@ -0,0 +1,116 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Subject } from 'rxjs/internal/Subject';
import { tap } from 'rxjs/operators';
export interface AvailableSkill {
icon: Object;
iconImage: string;
isMycroftMade: boolean;
isSystemSkill: boolean;
marketCategory: string;
name: string;
summary: string;
title: string;
trigger: string;
}
export interface SkillCredits {
name: string;
github_id: string;
}
export interface SkillDetail {
categories: string[];
credits: SkillCredits[];
description: string;
icon: Object;
iconImage: string;
isSystemSkill: boolean;
name: string;
repositoryUrl: string;
summary: string;
title: string;
triggers: string;
worksOnKDE: boolean;
worksOnMarkOne: boolean;
worksOnMarkTwo: boolean;
worksOnPicroft: boolean;
}
const availableSkillsUrl = '/api/skill/available';
const skillUrl = '/api/skill/detail/';
const searchQuery = '?search=';
@Injectable()
export class SkillsService {
public availableSkills: AvailableSkill[];
public isFiltered = new Subject<boolean>();
constructor(private http: HttpClient) { }
/**
* API call to retrieve all the skills available to the user
*/
getAvailableSkills(): Observable<AvailableSkill[]> {
return this.http.get<AvailableSkill[]>(availableSkillsUrl).pipe(
tap((skills) => { this.availableSkills = skills; })
);
}
/**
* Loop through the available skills to build a list of distinct categories.
*/
getSkillCategories(): string[] {
const orderedSkillCategories: string[] = [];
const skillCategories: string[] = [];
let systemCategoryFound = false;
this.availableSkills.forEach(
(skill) => {
if (!skillCategories.includes(skill.marketCategory)) {
skillCategories.push(skill.marketCategory);
}
}
);
skillCategories.sort();
// Make the 'System' category display last, if it exists
skillCategories.forEach(
category => {
if (category === 'System') {
systemCategoryFound = true;
} else {
orderedSkillCategories.push(category);
}
}
);
if (systemCategoryFound) {
orderedSkillCategories.push('System');
}
return orderedSkillCategories;
}
/**
* API call to retrieve detailed information about a specified skill.
*
* @param skillName: name of the skill to retrieve
*/
getSkillById(skillName: string): Observable<SkillDetail> {
return this.http.get<SkillDetail>(skillUrl + skillName);
}
/**
* API call to retrieve available skills that match the specified search term.
*
* @param searchTerm string used to search skills
*/
searchSkills(searchTerm: string): Observable<AvailableSkill[]> {
this.isFiltered.next(!!searchTerm);
return this.http.get<AvailableSkill[]>(
availableSkillsUrl + searchQuery + searchTerm
);
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 560 85" enable-background="new 0 0 560 85" xml:space="preserve">
<g>
<path fill="#FFFFFF" d="M8.8,27.3l20.6,45.7L50,27.3c0.7-1.7,2.4-2.7,4.2-2.7h0.1c2.6,0,4.6,2.1,4.6,4.6v49c0,1.6-1.3,2.9-2.9,2.9
h0c-1.6,0-2.9-1.3-2.9-2.9V58l0.5-26.3L32.5,79.1c-0.5,1.2-1.7,2-3.1,2h0c-1.3,0-2.5-0.8-3.1-2L5.2,32l0.5,26v20.3
c0,1.6-1.3,2.9-2.9,2.9h0c-1.6,0-2.9-1.3-2.9-2.9l0-49c0-2.6,2.1-4.6,4.6-4.6h0C6.4,24.6,8.1,25.7,8.8,27.3z"/>
<path fill="#FFFFFF" d="M99.6,55.4L116.7,26c0.5-0.9,1.4-1.4,2.4-1.4h0c2.2,0,3.6,2.4,2.4,4.3l-19.2,31.7v17.6
c0,1.6-1.3,2.9-2.9,2.9h0c-1.6,0-2.9-1.3-2.9-2.9V60.3l-19-31.5c-1.1-1.9,0.2-4.3,2.4-4.3h0c1,0,1.9,0.5,2.4,1.4L99.6,55.4z"/>
<path fill="#FFFFFF" d="M180.2,63.3c1.8,0,3.3,1.7,2.8,3.5c-1.1,4.3-3.2,7.7-6.3,10.3c-3.9,3.2-9.1,4.8-15.6,4.8
c-6.9,0-12.4-2.3-16.7-6.9c-4.3-4.6-6.4-11-6.4-19.1v-6.2c0-5.1,1-9.6,2.9-13.6c1.9-3.9,4.6-7,8.2-9.1c3.5-2.1,7.6-3.2,12.3-3.2
c6.5,0,11.6,1.6,15.5,4.9c3.1,2.6,5.1,6,6.2,10.4c0.4,1.8-1,3.5-2.8,3.5h0c-1.3,0-2.5-0.9-2.8-2.2c-0.8-3.3-2.2-6-4.4-7.9
c-2.6-2.4-6.5-3.6-11.6-3.6c-5.4,0-9.7,1.8-12.9,5.5c-3.1,3.7-4.7,8.8-4.7,15.4v6.4c0,6.4,1.5,11.5,4.6,15.2
c3.1,3.7,7.3,5.6,12.6,5.6c5.2,0,9.1-1.1,11.8-3.4c2.2-1.9,3.7-4.6,4.5-8.1C177.7,64.2,178.9,63.3,180.2,63.3L180.2,63.3z"/>
<path fill="#FFFFFF" d="M226,58.4h-17.2v19.8c0,1.6-1.3,2.9-2.9,2.9h0c-1.6,0-2.9-1.3-2.9-2.9V24.6h20c6.5,0,11.6,1.5,15.3,4.6
c3.7,3,5.5,7.2,5.5,12.5c0,3.7-1.1,7-3.4,9.7c-2.3,2.8-5.3,4.7-9,5.7L244,76.8c1.1,1.7,0,4-2.1,4.2l0,0c-1,0.1-2-0.4-2.5-1.3
L226,58.4z M208.9,53.5h15.3c4.2,0,7.6-1.1,10.1-3.2c2.5-2.2,3.8-5,3.8-8.4c0-3.8-1.3-6.7-3.9-8.9c-2.6-2.2-6.2-3.3-10.9-3.3h-14.4
V53.5z"/>
<path fill="#FFFFFF" d="M377.5,55.3H352v23c0,1.6-1.3,2.9-2.9,2.9l0,0c-1.6,0-2.9-1.3-2.9-2.9V24.6h35.4c1.4,0,2.5,1.1,2.5,2.5v0.1
c0,1.4-1.1,2.5-2.5,2.5H352v20.7h25.5c1.4,0,2.5,1.1,2.5,2.5v0C380,54.1,378.9,55.3,377.5,55.3z"/>
<path fill="#FFFFFF" d="M440.7,29.6h-18.1v48.6c0,1.6-1.3,2.9-2.9,2.9l0,0c-1.6,0-2.9-1.3-2.9-2.9V29.6h-18.1
c-1.4,0-2.5-1.1-2.5-2.5v0c0-1.4,1.1-2.5,2.5-2.5h42c1.4,0,2.5,1.1,2.5,2.5v0C443.2,28.5,442.1,29.6,440.7,29.6z"/>
<path fill="#FFFFFF" d="M525,66h-27.4l-5.3,13.3c-0.4,1-1.4,1.7-2.6,1.7h0c-2,0-3.3-2-2.6-3.8l20.4-50.1c0.6-1.6,2.2-2.6,3.8-2.6
l0,0c1.7,0,3.2,1,3.8,2.6l20.2,50.1c0.7,1.8-0.6,3.8-2.6,3.8h0c-1.1,0-2.1-0.7-2.6-1.7L525,66z M499.5,61.1H523l-11.7-29.5
L499.5,61.1z"/>
<path fill="#FFFFFF" d="M557.1,81.1L557.1,81.1c-1.6,0-2.9-1.3-2.9-2.9V27.5c0-1.6,1.3-2.9,2.9-2.9l0,0c1.6,0,2.9,1.3,2.9,2.9v50.7
C560,79.8,558.7,81.1,557.1,81.1z"/>
<g>
<path fill="#FFFFFF" d="M294.4,19.9c-18,0-32.6,14.6-32.6,32.6S276.5,85,294.4,85S327,70.5,327,52.5S312.4,19.9,294.4,19.9z
M294.4,80c-15.2,0-27.5-12.4-27.5-27.5c0-15.2,12.4-27.5,27.5-27.5c15.2,0,27.5,12.4,27.5,27.5C322,67.7,309.6,80,294.4,80z"/>
<path fill="#FFFFFF" d="M271.8,52.5c0,12.5,10.1,22.6,22.6,22.6V29.8C281.9,29.8,271.8,40,271.8,52.5z"/>
<circle fill="#FFFFFF" cx="294.4" cy="5.8" r="5.8"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg821" inkscape:version="0.91 r13725" sodipodi:docname="KDElogoBoxBlue.svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 40 40"
style="enable-background:new 0 0 40 40;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:#6C7A89;}
</style>
<sodipodi:namedview bordercolor="#666666" borderopacity="1.0" id="base" inkscape:bbox-nodes="true" inkscape:current-layer="layer1" inkscape:cx="62.936714" inkscape:cy="68.6291" inkscape:document-units="mm" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:snap-bbox="true" inkscape:window-height="2045" inkscape:window-maximized="1" inkscape:window-width="3840" inkscape:window-x="0" inkscape:window-y="0" inkscape:zoom="5.6" pagecolor="#ffffff" showgrid="true" units="px" width="128px">
<inkscape:grid id="grid1391" type="xygrid"></inkscape:grid>
</sodipodi:namedview>
<g>
<rect id="rect4157" x="5.6" y="5.6" class="st0" width="28.8" height="28.8"/>
<path id="path5692_2_-3" inkscape:connector-curvature="0" class="st1" d="M21.6,9.3l-3.7,0.4v15.1l3.6-0.5v-6.4l4.9,7.1l3.8-1.2
l-5-6.9l5-6.5l-3.9-0.9l-4.8,6.5L21.6,9.3z M13.3,13c0,0-0.1,0-0.1,0.1l-1.4,1.4c-0.1,0.1-0.1,0.2,0,0.2l1.7,2.8
c-0.3,0.5-0.5,1-0.7,1.6l-3.1,0.6c-0.1,0-0.1,0.1-0.1,0.2v2c0,0.1,0.1,0.2,0.1,0.2l3,0.7c0.2,0.7,0.4,1.3,0.7,1.9l-1.7,2.6
c0,0.1,0,0.2,0,0.2l1.4,1.4c0.1,0.1,0.2,0.1,0.2,0l2.7-1.7c0.5,0.3,1.1,0.6,1.7,0.7l0.6,3c0,0.1,0.1,0.1,0.2,0.1h2
c0.1,0,0.2-0.1,0.2-0.1l0.7-3.1c0.6-0.2,1.2-0.4,1.8-0.7l2.7,1.8c0.1,0,0.2,0,0.2,0l1.4-1.4c0.1-0.1,0.1-0.2,0-0.2l-1-1.6l-0.3,0.1
c0,0-0.1,0-0.1,0c0,0-0.6-0.9-1.4-2.1c-1,1.9-2.9,3.2-5.2,3.2c-3.2,0-5.8-2.6-5.8-5.8c0-2.4,1.4-4.4,3.4-5.3v-1.5
c-0.4,0.1-0.7,0.3-1.1,0.5c0,0,0,0,0,0L13.4,13C13.4,13,13.3,13,13.3,13L13.3,13z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View File

@ -0,0 +1,7 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M37.7801 9.38C37.7801 11.48 36.6301 13 34.5501 14.05C34.3901 14.96 34.2701 16.12 34.2601 16.27C34.2649 16.386 34.2283 16.5 34.1569 16.5916C34.0854 16.6832 33.9838 16.7464 33.8701 16.77C33.4945 16.8409 33.1119 16.8677 32.7301 16.85C32.2857 16.8524 31.8426 16.802 31.4101 16.7C31.0201 16.61 30.9901 16.5 30.9901 16.5C30.8806 16.1776 30.6954 15.8861 30.4501 15.65C30.3843 15.5725 30.3106 15.5021 30.2301 15.44C26.4847 16.0743 22.688 16.3555 18.8901 16.28C15.004 16.3662 11.1187 16.0715 7.29008 15.4C7.16467 15.4893 7.05051 15.5933 6.95009 15.71C6.70849 15.9433 6.52669 16.2315 6.42008 16.55C6.42008 16.55 6.42008 16.66 6.00008 16.76C5.56716 16.8586 5.12407 16.9056 4.68008 16.9C4.29496 16.9172 3.90912 16.8904 3.53008 16.82C3.41725 16.7962 3.31675 16.7326 3.24697 16.6408C3.17719 16.549 3.1428 16.4351 3.15008 16.32C3.15008 16.16 2.99008 14.76 2.81008 13.84C1.97279 13.4321 1.26607 12.7984 0.769591 12.0104C0.273107 11.2224 0.00659219 10.3114 8.47899e-05 9.38C-0.00235877 8.95211 0.0480221 8.52555 0.150085 8.11V8.11C0.650085 4.98 5.41009 1.84 12.6101 1.17C13.4601 1.09 14.5001 1.06 15.6801 1.06V0.24L15.8901 0C15.8901 0 17.0101 0 18.7301 0C20.4501 0 21.6701 0 21.6701 0L21.8901 0.21V1.05C23.4601 1.05 24.7601 1.11 25.5901 1.21C32.5901 2.02 37.0601 5.04 37.6501 8.03C37.7536 8.47217 37.7973 8.92621 37.7801 9.38V9.38Z" transform="translate(1 12)" stroke="#6C7A89" stroke-width="0.75" stroke-linejoin="round"/>
<path d="M35.7 6C35.7 11 27.7 12 17.85 12C8 12 0 10.84 0 6C0 1.16 8 0 17.85 0C27.7 0 35.7 1.06 35.7 6Z" transform="translate(2.04004 15.1602)" fill="#6C7A89"/>
<path d="M2.87 5.74C4.45506 5.74 5.74 4.45506 5.74 2.87C5.74 1.28494 4.45506 0 2.87 0C1.28494 0 0 1.28494 0 2.87C0 4.45506 1.28494 5.74 2.87 5.74Z" transform="translate(4.11035 18.3599)" stroke="white" stroke-linejoin="round"/>
<path d="M2.87 5.74C4.45506 5.74 5.74 4.45506 5.74 2.87C5.74 1.28494 4.45506 0 2.87 0C1.28495 0 0 1.28494 0 2.87C0 4.45506 1.28495 5.74 2.87 5.74Z" transform="translate(29.5996 18.3599)" stroke="white" stroke-linejoin="round"/>
<path d="M0 0L1.65 1.08H7.7L9.36 0H0Z" transform="translate(15.04 22.6299)" fill="white" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,6 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.52 29.8869C19.4771 30.5082 19.3766 31.1241 19.22 31.7269C19.2264 31.7532 19.2264 31.7806 19.22 31.8069C19.0677 32.3492 18.8874 32.8832 18.68 33.4069C18.57 33.6669 18.43 33.9369 18.29 34.1969C18.1326 34.4505 17.9554 34.6913 17.76 34.9169C17.5849 35.1432 17.3676 35.3334 17.12 35.4769C16.8678 35.6114 16.5858 35.6802 16.3 35.6769H3.53C3.24706 35.683 2.96756 35.614 2.72 35.4769C2.46971 35.3332 2.2491 35.1432 2.07 34.9169C1.88106 34.6909 1.71052 34.4501 1.56 34.1969C1.4 33.9369 1.28 33.6669 1.16 33.4069C1.02759 33.1247 0.914004 32.8341 0.820001 32.5369C0.820001 32.5369 0.700001 32.1169 0.650001 31.8969C0.650001 31.8369 0.43 30.7269 0.39 30.1369C0.0600003 25.3569 0 11.2269 0 11.2269C0 5.42689 3.17 0.00689305 9.88 0.00689305C11.2703 -0.0466351 12.655 0.212137 13.9321 0.764182C15.2092 1.31623 16.3464 2.14751 17.26 3.19689C18.9775 5.33145 19.8781 8.00826 19.8 10.7469C19.8 10.7469 19.89 25.1869 19.52 29.8869Z" transform="translate(10 2)" stroke="#6C7A89" stroke-width="0.75" stroke-linejoin="round"/>
<path d="M13.8432 19.94C13.8432 19.53 13.8432 19.11 13.8432 18.69V9.77C13.8432 9.62333 13.8432 9.47667 13.8432 9.33C13.8432 4.4 12.9932 0 7.00319 0C0.81319 0 0.00318953 4.47 0.00318953 9.45V19.06C0.00318953 19.49 0.00318953 19.93 0.00318953 20.35C-0.0288053 22.0015 0.180056 23.6488 0.623189 25.24C1.34319 27.77 3.15319 29.53 6.84319 29.59C12.8432 29.69 13.7332 25.01 13.7832 19.96L13.8432 19.94Z" transform="translate(13.0469 5.1167)" fill="#6C7A89"/>
<path d="M0.380812 1.96426C0.323184 1.96438 0.266283 1.9514 0.214414 1.92628C0.162545 1.90117 0.117067 1.86459 0.0814202 1.81931C0.0457737 1.77403 0.0208922 1.72123 0.00865958 1.66492C-0.00357306 1.6086 -0.00283688 1.55025 0.010812 1.49426C0.122676 1.06626 0.373284 0.687426 0.723414 0.417047C1.07354 0.146667 1.50344 0 1.94581 0C2.38819 0 2.81808 0.146667 3.16821 0.417047C3.51834 0.687426 3.76895 1.06626 3.88081 1.49426C3.89446 1.55025 3.8952 1.6086 3.88296 1.66492C3.87073 1.72123 3.84585 1.77403 3.8102 1.81931C3.77456 1.86459 3.72908 1.90117 3.67721 1.92628C3.62534 1.9514 3.56844 1.96438 3.51081 1.96426C3.42466 1.96216 3.34163 1.93158 3.27469 1.87731C3.20775 1.82303 3.16067 1.74812 3.14081 1.66426C3.072 1.40106 2.91787 1.1681 2.70255 1.00184C2.48722 0.835573 2.22286 0.745385 1.95081 0.745385C1.67877 0.745385 1.4144 0.835573 1.19908 1.00184C0.983753 1.1681 0.829625 1.40106 0.760812 1.66426C0.74095 1.74812 0.693873 1.82303 0.626934 1.87731C0.559995 1.93158 0.476964 1.96216 0.390812 1.96426H0.380812Z" transform="translate(14.4189 15.1528)" fill="white"/>
<path d="M0.380812 1.96426C0.323184 1.96438 0.266282 1.9514 0.214413 1.92628C0.162544 1.90117 0.117066 1.86459 0.0814196 1.81931C0.0457732 1.77403 0.0208927 1.72123 0.00866002 1.66492C-0.00357263 1.6086 -0.0028374 1.55025 0.0108115 1.49426C0.122675 1.06626 0.373284 0.687426 0.723414 0.417047C1.07354 0.146667 1.50344 0 1.94581 0C2.38819 0 2.81808 0.146667 3.16821 0.417047C3.51834 0.687426 3.76895 1.06626 3.88081 1.49426C3.89446 1.55025 3.8952 1.6086 3.88296 1.66492C3.87073 1.72123 3.84585 1.77403 3.8102 1.81931C3.77456 1.86459 3.72908 1.90117 3.67721 1.92628C3.62534 1.9514 3.56844 1.96438 3.51081 1.96426C3.42466 1.96216 3.34163 1.93158 3.27469 1.87731C3.20775 1.82303 3.16067 1.74812 3.14081 1.66426C3.072 1.40106 2.91787 1.1681 2.70255 1.00184C2.48722 0.835573 2.22286 0.745385 1.95081 0.745385C1.67877 0.745385 1.4144 0.835573 1.19908 1.00184C0.983753 1.1681 0.829625 1.40106 0.760812 1.66426C0.74095 1.74812 0.693872 1.82303 0.626933 1.87731C0.559994 1.93158 0.476965 1.96216 0.390813 1.96426H0.380812Z" transform="translate(21.6191 15.1528)" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1800 1800" enable-background="new 0 0 1800 1800" xml:space="preserve">
<g>
<path fill="#22A7F0" d="M493.3,195.6C305.9,303.8,120.6,450,120.6,450v0h0c0,0-33.9,233.6-33.9,450c0,216.4,33.9,450,33.9,450
s185.3,146.2,372.7,254.4C680.8,1712.6,900,1800,900,1800l0,0c0.1,0,219.3-87.4,406.7-195.6c187.4-108.2,372.7-254.4,372.7-254.4
s33.9-233.6,33.9-450c0-216.4-33.9-450-33.9-450s-185.3-146.2-372.7-254.4C1119.2,87.4,900,0,900,0S680.8,87.4,493.3,195.6z"/>
<path fill="#FFFFFF" d="M900,530c204,0,370,166,370,370s-166,370-370,370s-370-166-370-370S696,530,900,530 M900,462.8
c-241.5,0-437.2,195.8-437.2,437.2s195.8,437.2,437.2,437.2s437.2-195.8,437.2-437.2S1141.5,462.8,900,462.8L900,462.8z"/>
<path fill="#FFFFFF" d="M900,595.8C732,595.8,595.8,732,595.8,900S732,1204.2,900,1204.2V595.8z"/>
<circle fill="#FFFFFF" cx="900" cy="272.5" r="77.5"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,12 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.01 0H0.889999C0.398466 0 0 0.398466 0 0.889999V14.16C0 14.6515 0.398466 15.05 0.889999 15.05H22.01C22.5015 15.05 22.9 14.6515 22.9 14.16V0.889999C22.9 0.398466 22.5015 0 22.01 0Z" transform="translate(8 12)" fill="#6C7A89"/>
<path d="M4.47 0H0V3.5H4.47V0Z" transform="translate(27.1201 12.6602)" fill="white" stroke="#6C7A89" stroke-width="0.26" stroke-miterlimit="10"/>
<path d="M5.11 0H0V4.24H5.11V0Z" transform="translate(26.4902 21.9399)" fill="white" stroke="#6C7A89" stroke-width="0.25" stroke-miterlimit="10"/>
<path d="M0.75 1.5C1.16421 1.5 1.5 1.16421 1.5 0.75C1.5 0.335786 1.16421 0 0.75 0C0.335786 0 0 0.335786 0 0.75C0 1.16421 0.335786 1.5 0.75 1.5Z" transform="translate(9.0498 24.5601)" fill="white"/>
<path d="M0.75 1.5C1.16421 1.5 1.5 1.16421 1.5 0.75C1.5 0.335786 1.16421 0 0.75 0C0.335786 0 0 0.335786 0 0.75C0 1.16421 0.335786 1.5 0.75 1.5Z" transform="translate(24.0898 24.5601)" fill="white"/>
<path d="M0.75 1.5C1.16421 1.5 1.5 1.16421 1.5 0.75C1.5 0.335786 1.16421 0 0.75 0C0.335786 0 0 0.335786 0 0.75C0 1.16421 0.335786 1.5 0.75 1.5Z" transform="translate(24.0898 12.98)" fill="white"/>
<path d="M0.75 1.5C1.16421 1.5 1.5 1.16421 1.5 0.75C1.5 0.335786 1.16421 0 0.75 0C0.335786 0 0 0.335786 0 0.75C0 1.16421 0.335786 1.5 0.75 1.5Z" transform="translate(9.0498 12.98)" fill="white"/>
<path d="M4.47 0H0V3.5H4.47V0Z" transform="translate(27.1201 17.3501)" fill="white" stroke="#6C7A89" stroke-width="0.26" stroke-miterlimit="10"/>
<path d="M4.78 9.56C3.83461 9.56 2.91044 9.27966 2.12438 8.75443C1.33831 8.22919 0.725645 7.48266 0.363858 6.60923C0.00207138 5.7358 -0.0925889 4.7747 0.0918484 3.84747C0.276286 2.92024 0.731537 2.06853 1.40003 1.40003C2.06853 0.731536 2.92024 0.276286 3.84747 0.0918484C4.7747 -0.0925889 5.7358 0.00207138 6.60923 0.363858C7.48266 0.725645 8.22919 1.33831 8.75443 2.12438C9.27966 2.91044 9.56 3.83461 9.56 4.78C9.56 5.40772 9.43636 6.02929 9.19615 6.60923C8.95593 7.18917 8.60384 7.71611 8.15997 8.15997C7.71611 8.60384 7.18916 8.95593 6.60923 9.19615C6.02929 9.43636 5.40772 9.56 4.78 9.56Z" transform="translate(12.5195 14.6401)" fill="white"/>
<path d="M4 0C3.20888 0 2.43552 0.234596 1.77772 0.674122C1.11992 1.11365 0.607234 1.73836 0.304484 2.46927C0.00173312 3.20017 -0.0774802 4.00444 0.0768607 4.78036C0.231202 5.55629 0.612165 6.26902 1.17157 6.82843C1.73098 7.38784 2.44372 7.7688 3.21964 7.92314C3.99556 8.07748 4.79983 7.99827 5.53074 7.69552C6.26164 7.39277 6.88635 6.88008 7.32588 6.22228C7.76541 5.56448 8 4.79113 8 4C7.98703 2.94316 7.56144 1.93327 6.81409 1.18591C6.06674 0.43856 5.05684 0.0129688 4 0ZM4 7.38C3.11683 7.38 2.26983 7.02916 1.64534 6.40466C1.02084 5.78017 0.670002 4.93317 0.670002 4.05C0.670002 3.16683 1.02084 2.31983 1.64534 1.69534C2.26983 1.07084 3.11683 0.719999 4 0.719999V7.38Z" transform="translate(13.2998 15.3599)" fill="#6C7A89"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,4 @@
export const environment = {
production: false,
loginUrl: 'http://login.mycroft.test'
};

View File

@ -0,0 +1,16 @@
export const environment = {
production: true,
loginUrl: 'https://login.mycroft.ai'
};
document.write(
'<script async src="https://www.googletagmanager.com/gtag/js?id=UA-101772425-10"></script>'
);
document.write(
'<script>' +
'window.dataLayer = window.dataLayer || []; ' +
'function gtag(){dataLayer.push(arguments);} ' +
'gtag("js", new Date());' +
'gtag("config", "UA-101772425-10"); ' +
'</script>'
);

View File

@ -0,0 +1,4 @@
export const environment = {
production: false,
loginUrl: 'https://login.mycroft-test.net'
};

View File

@ -0,0 +1,17 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false,
apiUrl: 'http://localhost:5002',
loginUrl: 'http://localhost:4201'
};
/*
* In development mode, to ignore zone related error stack frames such as
* `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can
* import the following file, but please comment it out in production mode
* because it will have performance impact when throw error
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View File

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Mycroft Marketplace</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Roboto+Mono:300,400,500" rel="stylesheet">
<link href="https://use.fontawesome.com/releases/v5.3.1/css/all.css" integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU" crossorigin="anonymous" rel="stylesheet">
</head>
<body style="background-color: #f1f3f4; margin: 0">
<market-root></market-root>
</body>
</html>

View File

@ -0,0 +1,12 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

View File

@ -0,0 +1,80 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
// import 'core-js/es6/symbol';
// import 'core-js/es6/object';
// import 'core-js/es6/function';
// import 'core-js/es6/parse-int';
// import 'core-js/es6/parse-float';
// import 'core-js/es6/number';
// import 'core-js/es6/math';
// import 'core-js/es6/string';
// import 'core-js/es6/date';
// import 'core-js/es6/array';
// import 'core-js/es6/regexp';
// import 'core-js/es6/map';
// import 'core-js/es6/weak-map';
// import 'core-js/es6/set';
/**
* If the application will be indexed by Google Search, the following is required.
* Googlebot uses a renderer based on Chrome 41.
* https://developers.google.com/search/docs/guides/rendering
**/
// import 'core-js/es6/array';
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/** IE10 and IE11 requires the following for the Reflect API. */
// import 'core-js/es6/reflect';
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
**/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
*/
// (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
// (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
// (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
/*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*/
// (window as any).__Zone_enable_cross_context_check = true;
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

View File

@ -0,0 +1,95 @@
/* You can add global styles to this file, and also import other style files */
@import '~@angular/material/theming';
// Be sure that you only ever include 'mat-core' mixin once!
// it should not be included for each theme.
@include mat-core();
// Mycroft palette defined using http://mcg.mbitson.com
$mycroft-color-primary: (
50 : #e4f4fd,
100 : #bde5fb,
200 : #91d3f8,
300 : #64c1f5,
400 : #43b4f2,
500 : #22a7f0,
600 : #1e9fee,
700 : #1996ec,
800 : #148ce9,
900 : #0c7ce5,
A100 : #ffffff,
A200 : #dcedff,
A400 : #a9d2ff,
A700 : #90c5ff,
contrast: (
50 : #000000,
100 : #000000,
200 : #000000,
300 : #000000,
400 : #000000,
500 : #000000,
600 : #000000,
700 : #ffffff,
800 : #ffffff,
900 : #ffffff,
A100 : #000000,
A200 : #000000,
A400 : #000000,
A700 : #000000,
)
);
$mycroft-color-secondary: (
50 : #e6e8ea,
100 : #c0c5cb,
200 : #969fa8,
300 : #6b7885,
400 : #4c5b6a,
500 : #2c3e50,
600 : #273849,
700 : #213040,
800 : #1b2837,
900 : #101b27,
A100 : #68abff,
A200 : #358fff,
A400 : #0272ff,
A700 : #0067e7,
contrast: (
50 : #000000,
100 : #000000,
200 : #000000,
300 : #ffffff,
400 : #ffffff,
500 : #ffffff,
600 : #ffffff,
700 : #ffffff,
800 : #ffffff,
900 : #ffffff,
A100 : #000000,
A200 : #000000,
A400 : #ffffff,
A700 : #ffffff,
)
);
// mandatory stuff for theming
$mycroft-palette-primary: mat-palette($mycroft-color-primary);
$mycroft-palette-accent: mat-palette($mycroft-color-secondary);
// include the custom theme components into a theme object
$mycroft-theme: mat-light-theme($mycroft-palette-primary, $mycroft-palette-accent);
// include the custom theme object into the angular material theme
@include angular-material-theme($mycroft-theme);
body {
background-color: #f1f1f1;
}
.mycroft-snackbar {
width: 500px;
}
.mycroft-snackbar .mat-simple-snackbar-action {
color: #22a7f0
}

View File

@ -0,0 +1,14 @@
// These are the official Mycroft colors as defined by the design team.
$mycroft-primary: #22a7f0;
$mycroft-secondary: #2c3e50;
$mycroft-tertiary-blue: #96defe;
$mycroft-tertiary-green: #40dbb0;
$mycroft-tertiary-yellow: #fee255;
$mycroft-tertiary-grey: #5b6984;
$mycroft-tertiary-orange: #fd9e66;
$mycroft-white: #ffffff;
$mycroft-black: #111111;
$mycroft-dark-grey: #6c7a89;
$mycroft-light-grey: #bdc3c7;
$mycroft-blue-grey: #e4f1fe;
$market-background: #f1f3f4;

View File

@ -0,0 +1,9 @@
@import "../base/mycroft-colors";
$button-border-radius: 4px;
@mixin action-button {
border-radius: $button-border-radius;
background-color: $mycroft-primary;
color: $mycroft-white;
letter-spacing: 0.5px;
}

View File

@ -0,0 +1,22 @@
@import "../base/mycroft-colors";
@mixin ellipsis-overflow {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@mixin skill-trigger {
background-color: $mycroft-blue-grey;
border-radius: 4px;
color: $mycroft-secondary;
font-weight: normal;
padding-bottom: 7px;
padding-left: 12px;
padding-right: 12px;
padding-top: 7px;
fa-icon {
color: $mycroft-primary;
margin-right: 5px;
}
}

View File

@ -0,0 +1,3 @@
@import "base/mycroft-colors";
@import "components/buttons";
@import "components/text";

View File

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

View File

@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/app",
"types": []
},
"exclude": [
"test.ts",
"**/*.spec.ts"
]
}

View File

@ -0,0 +1,18 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/spec",
"types": [
"jasmine",
"node"
]
},
"files": [
"src/test.ts",
"src/polyfills.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}

View File

@ -0,0 +1,17 @@
{
"extends": "../../tslint.json",
"rules": {
"directive-selector": [
true,
"attribute",
"market",
"camelCase"
],
"component-selector": [
true,
"element",
"market",
"kebab-case"
]
}
}

View File

@ -0,0 +1,10 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

@ -0,0 +1,21 @@
<!--The content below is only a placeholder and can be replaced.-->
<div style="text-align:center">
<h1>
Welcome to {{ title }}!
</h1>
<img width="300" alt="Angular Logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==">
</div>
<h2>Here are some links to help you start: </h2>
<ul>
<li>
<h2><a target="_blank" rel="noopener" href="https://angular.io/tutorial">Tour of Heroes</a></h2>
</li>
<li>
<h2><a target="_blank" rel="noopener" href="https://github.com/angular/angular-cli/wiki">CLI Documentation</a></h2>
</li>
<li>
<h2><a target="_blank" rel="noopener" href="https://blog.angular.io/">Angular blog</a></h2>
</li>
</ul>
<router-outlet></router-outlet>

View File

View File

@ -0,0 +1,35 @@
import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'internet'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('internet');
});
it('should render title in a h1 tag', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to internet!');
});
});

10
src/app/app.component.ts Normal file
View File

@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'internet';
}

18
src/app/app.module.ts Normal file
View File

@ -0,0 +1,18 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

0
src/assets/.gitkeep Normal file
View File

11
src/browserslist Normal file
View File

@ -0,0 +1,11 @@
# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
#
# For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed
> 0.5%
last 2 versions
Firefox ESR
not dead
not IE 9-11

View File

@ -0,0 +1,3 @@
export const environment = {
production: true
};

View File

@ -0,0 +1,16 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.

14
src/index.html Normal file
View File

@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Internet</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>

31
src/karma.conf.js Normal file
View File

@ -0,0 +1,31 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../coverage'),
reports: ['html', 'lcovonly'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false
});
};

12
src/main.ts Normal file
View File

@ -0,0 +1,12 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

80
src/polyfills.ts Normal file
View File

@ -0,0 +1,80 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
// import 'core-js/es6/symbol';
// import 'core-js/es6/object';
// import 'core-js/es6/function';
// import 'core-js/es6/parse-int';
// import 'core-js/es6/parse-float';
// import 'core-js/es6/number';
// import 'core-js/es6/math';
// import 'core-js/es6/string';
// import 'core-js/es6/date';
// import 'core-js/es6/array';
// import 'core-js/es6/regexp';
// import 'core-js/es6/map';
// import 'core-js/es6/weak-map';
// import 'core-js/es6/set';
/**
* If the application will be indexed by Google Search, the following is required.
* Googlebot uses a renderer based on Chrome 41.
* https://developers.google.com/search/docs/guides/rendering
**/
// import 'core-js/es6/array';
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/** IE10 and IE11 requires the following for the Reflect API. */
// import 'core-js/es6/reflect';
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
**/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
*/
// (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
// (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
// (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
/*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*/
// (window as any).__Zone_enable_cross_context_check = true;
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

1
src/styles.scss Normal file
View File

@ -0,0 +1 @@
/* You can add global styles to this file, and also import other style files */

20
src/test.ts Normal file
View File

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

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