diff --git a/packages/uhk-common/src/util/constants.ts b/packages/uhk-common/src/util/constants.ts index f5d1912c..1c6f4c5a 100644 --- a/packages/uhk-common/src/util/constants.ts +++ b/packages/uhk-common/src/util/constants.ts @@ -1,4 +1,6 @@ export namespace Constants { export const AGENT_GITHUB_URL = 'https://github.com/UltimateHackingKeyboard/agent'; + export const AGENT_CONTRIBUTORS_GITHUB_PAGE_URL = 'https://github.com/UltimateHackingKeyboard/agent/graphs/contributors'; + export const AGENT_CONTRIBUTORS_GITHUB_API_URL = 'https://api.github.com/repos/UltimateHackingKeyboard/agent/contributors'; export const FIRMWARE_GITHUB_ISSUE_URL = 'https://github.com/UltimateHackingKeyboard/agent/issues/new'; } diff --git a/packages/uhk-web/src/app/components/agent/about/about.component.html b/packages/uhk-web/src/app/components/agent/about/about.component.html index 913dab51..7df1e8f0 100644 --- a/packages/uhk-web/src/app/components/agent/about/about.component.html +++ b/packages/uhk-web/src/app/components/agent/about/about.component.html @@ -1,10 +1,44 @@
-

- - About +

+

-
-
Agent version: {{ version }}
-
Agent on GitHub
+
+
+
+
+ The configurator of the Ultimate Hacking Keyboard +
+
+
+ Agent version: {{ version }} +
+ Agent on GitHub +
+ +
+ Created by Ultimate Gadget Laboratories and its awesome contributors: +
+
+
+
+ +
+
+
+ +
+ Loading... +
+
+ +
+ Loading... +
+
+ We experienced a problem while fetching contributor list. Check Contributors page on GitHub! +
+
+
+
May the UHK be with you!
diff --git a/packages/uhk-web/src/app/components/agent/about/about.component.scss b/packages/uhk-web/src/app/components/agent/about/about.component.scss index 9816fb38..1155a324 100644 --- a/packages/uhk-web/src/app/components/agent/about/about.component.scss +++ b/packages/uhk-web/src/app/components/agent/about/about.component.scss @@ -5,16 +5,12 @@ width: 100%; } -.agent { - &-version { - margin-bottom: 1rem; - - span { - font-weight: bold; - } - } -} - .link-github { cursor: pointer; } + +contributor-badge { + display: block; + margin: 0.5em auto; + min-width: 12em; +} diff --git a/packages/uhk-web/src/app/components/agent/about/about.component.ts b/packages/uhk-web/src/app/components/agent/about/about.component.ts index 3c6528ff..dcb43b94 100644 --- a/packages/uhk-web/src/app/components/agent/about/about.component.ts +++ b/packages/uhk-web/src/app/components/agent/about/about.component.ts @@ -1,8 +1,16 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs/Observable'; + import { Constants } from 'uhk-common'; import { getVersions } from '../../../util'; +import { AppState, contributors } from '../../../store'; +import { State } from '../../../store/reducers/contributors.reducer'; +import { OpenUrlInNewWindowAction } from '../../../store/actions/app'; +import { GetAgentContributorsAction } from '../../../store/actions/contributors.action'; + @Component({ selector: 'about-page', templateUrl: './about.component.html', @@ -11,7 +19,24 @@ import { getVersions } from '../../../util'; 'class': 'container-fluid' } }) -export class AboutComponent { +export class AboutComponent implements OnInit { version: string = getVersions().version; agentGithubUrl = Constants.AGENT_GITHUB_URL; + agentContributorsUrl = Constants.AGENT_CONTRIBUTORS_GITHUB_PAGE_URL; + state$: Observable; + + constructor(private store: Store) { + } + + ngOnInit() { + this.state$ = this.store.select(contributors); + + this.store.dispatch(new GetAgentContributorsAction()); + } + + openUrlInBrowser(event: Event): void { + event.preventDefault(); + + this.store.dispatch(new OpenUrlInNewWindowAction((event.target as Element).getAttribute('href'))); + } } diff --git a/packages/uhk-web/src/app/components/agent/about/contributor-badge/contributor-badge.component.html b/packages/uhk-web/src/app/components/agent/about/contributor-badge/contributor-badge.component.html new file mode 100644 index 00000000..163ee5ea --- /dev/null +++ b/packages/uhk-web/src/app/components/agent/about/contributor-badge/contributor-badge.component.html @@ -0,0 +1,2 @@ +{{ name }} on GitHub +{{ name }} \ No newline at end of file diff --git a/packages/uhk-web/src/app/components/agent/about/contributor-badge/contributor-badge.component.scss b/packages/uhk-web/src/app/components/agent/about/contributor-badge/contributor-badge.component.scss new file mode 100644 index 00000000..81ea9819 --- /dev/null +++ b/packages/uhk-web/src/app/components/agent/about/contributor-badge/contributor-badge.component.scss @@ -0,0 +1,8 @@ +:host { + img { + width: 36px; + height: 36px; + border-radius: 3px; + margin-right: 5px; + } +} diff --git a/packages/uhk-web/src/app/components/agent/about/contributor-badge/contributor-badge.component.ts b/packages/uhk-web/src/app/components/agent/about/contributor-badge/contributor-badge.component.ts new file mode 100644 index 00000000..e3ccfb1b --- /dev/null +++ b/packages/uhk-web/src/app/components/agent/about/contributor-badge/contributor-badge.component.ts @@ -0,0 +1,42 @@ +import { Component, Input, ViewChild, ElementRef, OnInit } from '@angular/core'; +import { Store } from '@ngrx/store'; + +import { AppState } from '../../../../store/index'; +import { OpenUrlInNewWindowAction } from '../../../../store/actions/app'; + +import { UHKContributor } from '../../../../models/uhk-contributor'; + +@Component({ + selector: 'contributor-badge', + templateUrl: './contributor-badge.component.html', + styleUrls: ['./contributor-badge.component.scss'] +}) +export class ContributorBadgeComponent implements OnInit { + @Input() contributor: UHKContributor; + @ViewChild('badge') badge: ElementRef; + + get name(): string { + return this.contributor.login; + } + + get avatarUrl(): string { + return this.contributor.avatar_url; + } + + get profileUrl(): string { + return this.contributor.html_url; + } + + constructor(private store: Store) { + } + + ngOnInit(): void { + (this.badge.nativeElement as HTMLImageElement).src = URL.createObjectURL(this.contributor.avatar); + } + + openUrlInBrowser(event: Event): void { + event.preventDefault(); + + this.store.dispatch(new OpenUrlInNewWindowAction((event.target as Element).getAttribute('href'))); + } +} diff --git a/packages/uhk-web/src/app/components/agent/index.ts b/packages/uhk-web/src/app/components/agent/index.ts index aa484a2d..b5a0c64b 100644 --- a/packages/uhk-web/src/app/components/agent/index.ts +++ b/packages/uhk-web/src/app/components/agent/index.ts @@ -1,3 +1,4 @@ export * from './agent.routes'; export * from './about/about.component'; +export * from './about/contributor-badge/contributor-badge.component'; export * from './settings/settings.component'; diff --git a/packages/uhk-web/src/app/models/uhk-contributor.ts b/packages/uhk-web/src/app/models/uhk-contributor.ts new file mode 100644 index 00000000..4f2e888e --- /dev/null +++ b/packages/uhk-web/src/app/models/uhk-contributor.ts @@ -0,0 +1,7 @@ +export interface UHKContributor { + login: string; + avatar_url: string; + html_url: string; + contributions: number; + avatar: Blob; +} diff --git a/packages/uhk-web/src/app/shared.module.ts b/packages/uhk-web/src/app/shared.module.ts index ea863038..f6f35785 100644 --- a/packages/uhk-web/src/app/shared.module.ts +++ b/packages/uhk-web/src/app/shared.module.ts @@ -2,6 +2,7 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { HttpClientModule } from '@angular/common/http'; import { NotifierModule } from 'angular-notifier'; import { ConfirmationPopoverModule } from 'angular-confirmation-popover'; @@ -46,7 +47,7 @@ import { } from './components/popover/tab'; import { CaptureKeystrokeButtonComponent } from './components/popover/widgets/capture-keystroke'; import { IconComponent } from './components/popover/widgets/icon'; -import { AboutComponent, SettingsComponent } from './components/agent'; +import { AboutComponent, SettingsComponent, ContributorBadgeComponent } from './components/agent'; import { SideMenuComponent } from './components/side-menu'; import { SvgKeyboardComponent } from './components/svg/keyboard'; import { @@ -164,6 +165,7 @@ import { UdevRulesComponent } from './components/udev-rules/udev-rules.component MacroNotFoundComponent, AddOnComponent, AboutComponent, + ContributorBadgeComponent, SettingsComponent, KeyboardSliderComponent, CancelableDirective, @@ -204,7 +206,8 @@ import { UdevRulesComponent } from './components/udev-rules/udev-rules.component ConfirmationPopoverModule.forRoot({ confirmButtonType: 'danger' // set defaults here }), - ClipboardModule + ClipboardModule, + HttpClientModule ], providers: [ SvgModuleProviderService, diff --git a/packages/uhk-web/src/app/store/actions/contributors.action.ts b/packages/uhk-web/src/app/store/actions/contributors.action.ts new file mode 100644 index 00000000..c76cda70 --- /dev/null +++ b/packages/uhk-web/src/app/store/actions/contributors.action.ts @@ -0,0 +1,44 @@ +import { Action } from '@ngrx/store'; + +import { type } from 'uhk-common'; + +import { UHKContributor } from '../../models/uhk-contributor'; + +const PREFIX = '[contributors] '; + +// tslint:disable-next-line:variable-name +export const ActionTypes = { + GET_AGENT_CONTRIBUTORS: type(PREFIX + 'Get'), + FETCH_AGENT_CONTRIBUTORS: type(PREFIX + 'Fetch'), + AGENT_CONTRIBUTORS_AVAILABLE: type(PREFIX + 'Available'), + AGENT_CONTRIBUTORS_NOT_AVAILABLE: type(PREFIX + 'Not available') +}; + +export class GetAgentContributorsAction implements Action { + type = ActionTypes.GET_AGENT_CONTRIBUTORS; +} +export class FetchAgentContributorsAction implements Action { + type = ActionTypes.FETCH_AGENT_CONTRIBUTORS; +} + +export class AgentContributorsAvailableAction implements Action { + type = ActionTypes.AGENT_CONTRIBUTORS_AVAILABLE; + + constructor(public payload: UHKContributor[]) { + } +} + +export class AgentContributorsNotAvailableAction implements Action { + type = ActionTypes.AGENT_CONTRIBUTORS_NOT_AVAILABLE; + + constructor(public payload: Error) { + console.error(payload); + } +} + +export type Actions + = FetchAgentContributorsAction + | AgentContributorsAvailableAction + | AgentContributorsNotAvailableAction + | FetchAgentContributorsAction + | GetAgentContributorsAction; diff --git a/packages/uhk-web/src/app/store/effects/contributors.effect.ts b/packages/uhk-web/src/app/store/effects/contributors.effect.ts new file mode 100644 index 00000000..360a850d --- /dev/null +++ b/packages/uhk-web/src/app/store/effects/contributors.effect.ts @@ -0,0 +1,67 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Actions, Effect } from '@ngrx/effects'; +import { Action, Store } from '@ngrx/store'; + +import { Observable } from 'rxjs/Observable'; +import { from } from 'rxjs/observable/from'; +import { of } from 'rxjs/observable/of'; +import { map, switchMap, catchError, reduce, mergeMap, withLatestFrom } from 'rxjs/operators'; + +import { Constants } from 'uhk-common'; + +import { AppState, contributors } from '../index'; +import { UHKContributor } from '../../models/uhk-contributor'; +import { + AgentContributorsAvailableAction, + AgentContributorsNotAvailableAction, + GetAgentContributorsAction, + ActionTypes, + FetchAgentContributorsAction +} from '../actions/contributors.action'; + +@Injectable() +export class ContributorsEffect { + @Effect() getContributors$: Observable = this.actions$ + .ofType(ActionTypes.GET_AGENT_CONTRIBUTORS) + .pipe( + withLatestFrom(this.store.select(contributors)), + map(([action, state]) => { + if (state.contributors.length === 0) { + return new FetchAgentContributorsAction(); + } + return new AgentContributorsAvailableAction(state.contributors); + }) + ); + + @Effect() fetchContributors$: Observable = this.actions$ + .ofType(ActionTypes.FETCH_AGENT_CONTRIBUTORS) + .pipe( + mergeMap(() => this.http.get(Constants.AGENT_CONTRIBUTORS_GITHUB_API_URL)), + switchMap((response: UHKContributor[]) => { + return from(response).pipe( + mergeMap( + (contributor: UHKContributor) => { + return this.http.get(contributor.avatar_url, { responseType: 'blob' }); + }, + (contributor, blob) => { + contributor.avatar = blob; + + return contributor; + } + ), + reduce((acc: UHKContributor[], curr) => [...acc, curr], []) + ); + }), + map( + (contributorsWithAvatars: UHKContributor[]) => { + contributorsWithAvatars = contributorsWithAvatars.sort((a, b) => b.contributions - a.contributions); + + return new AgentContributorsAvailableAction(contributorsWithAvatars); + } + ), + catchError(error => of(new AgentContributorsNotAvailableAction(error))) + ); + + constructor(private store: Store, private actions$: Actions, private http: HttpClient) {} +} diff --git a/packages/uhk-web/src/app/store/effects/index.ts b/packages/uhk-web/src/app/store/effects/index.ts index c37c658f..29e2768c 100644 --- a/packages/uhk-web/src/app/store/effects/index.ts +++ b/packages/uhk-web/src/app/store/effects/index.ts @@ -5,6 +5,7 @@ import { KeymapEffects } from './keymap'; import { UserConfigEffects } from './user-config'; import { ApplicationEffects } from './app'; import { AppUpdateEffect } from './app-update'; +import { ContributorsEffect } from './contributors.effect'; export * from './keymap'; export * from './macro'; @@ -19,5 +20,6 @@ export const effects = [ KeymapEffects, MacroEffects, AutoUpdateSettingsEffects, - DeviceEffects + DeviceEffects, + ContributorsEffect ]; diff --git a/packages/uhk-web/src/app/store/index.ts b/packages/uhk-web/src/app/store/index.ts index d5ecf224..ddf49696 100644 --- a/packages/uhk-web/src/app/store/index.ts +++ b/packages/uhk-web/src/app/store/index.ts @@ -7,6 +7,7 @@ import { HardwareModules, Keymap, UserConfiguration } from 'uhk-common'; import * as fromUserConfig from './reducers/user-configuration'; import * as fromPreset from './reducers/preset'; import * as fromAppUpdate from './reducers/app-update.reducer'; +import * as fromContributors from './reducers/contributors.reducer'; import * as autoUpdateSettings from './reducers/auto-update-settings'; import * as fromApp from './reducers/app.reducer'; import * as fromDevice from './reducers/device'; @@ -26,6 +27,7 @@ export interface AppState { router: RouterReducerState; appUpdate: fromAppUpdate.State; device: fromDevice.State; + contributors: fromContributors.State; } export const reducers: ActionReducerMap = { @@ -35,7 +37,8 @@ export const reducers: ActionReducerMap = { app: fromApp.reducer, router: routerReducer, appUpdate: fromAppUpdate.reducer, - device: fromDevice.reducer + device: fromDevice.reducer, + contributors: fromContributors.reducer }; export const metaReducers: MetaReducer[] = environment.production @@ -56,6 +59,7 @@ export const getAgentVersionInfo = createSelector(appState, fromApp.getAgentVers export const getOperatingSystem = createSelector(appState, fromSelectors.getOperatingSystem); export const keypressCapturing = createSelector(appState, fromApp.keypressCapturing); export const runningOnNotSupportedWindows = createSelector(appState, fromApp.runningOnNotSupportedWindows); +export const contributors = (state: AppState) => state.contributors; export const firmwareUpgradeAllowed = createSelector(runningOnNotSupportedWindows, notSupportedOs => !notSupportedOs); export const appUpdateState = (state: AppState) => state.appUpdate; diff --git a/packages/uhk-web/src/app/store/reducers/app.reducer.ts b/packages/uhk-web/src/app/store/reducers/app.reducer.ts index 6f1fc950..1928d1d8 100644 --- a/packages/uhk-web/src/app/store/reducers/app.reducer.ts +++ b/packages/uhk-web/src/app/store/reducers/app.reducer.ts @@ -16,7 +16,6 @@ import { ActionTypes as UserConfigActionTypes } from '../actions/user-config'; import { ActionTypes as DeviceActionTypes } from '../actions/device'; import { KeyboardLayout } from '../../keyboard/keyboard-layout.enum'; import { getVersions } from '../../util'; -import { PrivilagePageSate } from '../../models/privilage-page-sate'; export interface State { started: boolean; diff --git a/packages/uhk-web/src/app/store/reducers/contributors.reducer.ts b/packages/uhk-web/src/app/store/reducers/contributors.reducer.ts new file mode 100644 index 00000000..23881774 --- /dev/null +++ b/packages/uhk-web/src/app/store/reducers/contributors.reducer.ts @@ -0,0 +1,51 @@ +import { Actions, ActionTypes } from '../actions/contributors.action'; +import { AgentContributorsAvailableAction, AgentContributorsNotAvailableAction } from '../actions/contributors.action'; +import { UHKContributor } from '../../models/uhk-contributor'; + +export interface State { + isLoading: boolean; + contributors: UHKContributor[]; + error?: any; +} + +export const initialState: State = { + isLoading: false, + contributors: [], + error: null +}; + +export function reducer(state = initialState, action: Actions) { + switch (action.type) { + case ActionTypes.GET_AGENT_CONTRIBUTORS: { + return { + ...state + }; + } + + case ActionTypes.FETCH_AGENT_CONTRIBUTORS: { + return { + ...state, + isLoading: true + }; + } + + case ActionTypes.AGENT_CONTRIBUTORS_AVAILABLE: { + return { + ...state, + contributors: (action).payload, + isLoading: false + }; + } + + case ActionTypes.AGENT_CONTRIBUTORS_NOT_AVAILABLE: { + return { + ...state, + error: (action).payload, + isLoading: false + }; + } + + default: + return state; + } +} diff --git a/packages/uhk-web/src/styles.scss b/packages/uhk-web/src/styles.scss index c7eb859d..6022a804 100644 --- a/packages/uhk-web/src/styles.scss +++ b/packages/uhk-web/src/styles.scss @@ -20,12 +20,20 @@ html, body { background: url('assets/images/agent-icon.png') no-repeat; } +.uhk-icon-full-agent-icon { + background: url('assets/images/agent-logo-with-text.svg') no-repeat; +} + .uhk-icon { display: inline-block; width: 1em; height: 1em; background-size: auto 100%; vertical-align: text-bottom; + + &.wide { + width: 4.15em; + } } .rotate-right {