diff --git a/electron/src/app.module.ts b/electron/src/app.module.ts index f6134b99..68d01baf 100644 --- a/electron/src/app.module.ts +++ b/electron/src/app.module.ts @@ -10,6 +10,7 @@ import { StoreLogMonitorModule, useLogMonitor } from '@ngrx/store-log-monitor'; import { DragulaModule } from 'ng2-dragula/ng2-dragula'; import { Select2Module } from 'ng2-select2/ng2-select2'; +import { MissingDeviceComponent } from './components/missing-device/missing-device.component'; import { AddOnComponent } from './shared/components/add-on'; import { KeyboardSliderComponent } from './shared/components/keyboard/slider'; import { KeymapAddComponent, KeymapHeaderComponent } from './shared/components/keymap'; @@ -59,7 +60,9 @@ import { } from './shared/components/svg/keys'; import { SvgModuleComponent } from './shared/components/svg/module'; import { SvgKeyboardWrapComponent } from './shared/components/svg/wrap'; -import { MainAppComponent, appRoutingProviders, routing } from './main-app'; +import { appRoutingProviders, routing } from './app/app.routes'; +import { AppComponent } from './app/app.component'; +import { MainAppComponent } from './main-app'; import { CancelableDirective } from './shared/directives'; @@ -74,6 +77,8 @@ import { DataStorage } from './shared/store/storage'; import { KeymapEditGuard } from './shared/components/keymap/edit'; import { MacroNotFoundGuard } from './shared/components/macro/not-found'; +import { UHkConnectedGuard } from './services/uhk-connected.guard'; + // Create DataStorage dependency injection const storageProvider = ReflectiveInjector.resolve([DataStorage]); const storageInjector = ReflectiveInjector.fromResolvedProviders(storageProvider); @@ -87,6 +92,7 @@ const storeConfig = { @NgModule({ declarations: [ + AppComponent, MainAppComponent, KeymapEditComponent, KeymapHeaderComponent, @@ -133,6 +139,7 @@ const storeConfig = { AddOnComponent, SettingsComponent, KeyboardSliderComponent, + MissingDeviceComponent, CancelableDirective ], imports: [ @@ -153,6 +160,7 @@ const storeConfig = { EffectsModule.runAfterBootstrap(MacroEffects) ], providers: [ + UHkConnectedGuard, MapperService, appRoutingProviders, KeymapEditGuard, @@ -160,6 +168,6 @@ const storeConfig = { CaptureService, UhkDeviceService ], - bootstrap: [MainAppComponent] + bootstrap: [AppComponent] }) export class AppModule { } diff --git a/electron/src/app/app.component.html b/electron/src/app/app.component.html new file mode 100644 index 00000000..0680b43f --- /dev/null +++ b/electron/src/app/app.component.html @@ -0,0 +1 @@ + diff --git a/electron/src/app/app.component.scss b/electron/src/app/app.component.scss new file mode 100644 index 00000000..418ad7b4 --- /dev/null +++ b/electron/src/app/app.component.scss @@ -0,0 +1,6 @@ +app { + display: block; + min-height: 100vh; + height: 100vh; + width: 100%; +} diff --git a/electron/src/app/app.component.ts b/electron/src/app/app.component.ts new file mode 100644 index 00000000..ccce4074 --- /dev/null +++ b/electron/src/app/app.component.ts @@ -0,0 +1,9 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + selector: 'app', + templateUrl: 'app.component.html', + styleUrls: ['app.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class AppComponent { } diff --git a/electron/src/app/app.routes.ts b/electron/src/app/app.routes.ts new file mode 100644 index 00000000..fccb65fc --- /dev/null +++ b/electron/src/app/app.routes.ts @@ -0,0 +1,24 @@ +import { UHkConnectedGuard } from './../services/uhk-connected.guard'; +import { ModuleWithProviders } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { MissingDeviceComponent } from './../components/missing-device/missing-device.component'; +import { MainAppComponent } from './../main-app/main-app.component'; +import { mainAppRoutes } from './../main-app/main-app.routes'; + +const appRoutes: Routes = [ + { + path: 'detection', + component: MissingDeviceComponent + }, + { + path: '', + component: MainAppComponent, + canActivate: [UHkConnectedGuard], + children: mainAppRoutes + } +]; + +export const appRoutingProviders: any[] = []; + +export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes, { useHash: true }); diff --git a/electron/src/app/index.ts b/electron/src/app/index.ts new file mode 100644 index 00000000..df2f2075 --- /dev/null +++ b/electron/src/app/index.ts @@ -0,0 +1 @@ +export * from './app.component'; diff --git a/electron/src/components/keymap/keymap.routes.ts b/electron/src/components/keymap/keymap.routes.ts index 7df02708..013682f7 100644 --- a/electron/src/components/keymap/keymap.routes.ts +++ b/electron/src/components/keymap/keymap.routes.ts @@ -7,7 +7,7 @@ import { KeymapEditGuard } from '../../shared/components/keymap/edit'; export const keymapRoutes: Routes = [ { path: '', - redirectTo: '/keymap', + redirectTo: 'keymap', pathMatch: 'full' }, { diff --git a/electron/src/components/missing-device/index.ts b/electron/src/components/missing-device/index.ts new file mode 100644 index 00000000..3672a855 --- /dev/null +++ b/electron/src/components/missing-device/index.ts @@ -0,0 +1 @@ +export * from './missing-device.component'; diff --git a/electron/src/components/missing-device/missing-device.component.html b/electron/src/components/missing-device/missing-device.component.html new file mode 100644 index 00000000..55a0fef0 --- /dev/null +++ b/electron/src/components/missing-device/missing-device.component.html @@ -0,0 +1,7 @@ + + +
+

Cannot find your UHK

+

Please plug it in!

+
+
diff --git a/electron/src/components/missing-device/missing-device.component.scss b/electron/src/components/missing-device/missing-device.component.scss new file mode 100644 index 00000000..3a204fa2 --- /dev/null +++ b/electron/src/components/missing-device/missing-device.component.scss @@ -0,0 +1,19 @@ +.missing-device-wrapper { + display: flex; + height: 100%; + align-items: center; + justify-content: center; +} + +.agent-logo { + padding: 2em; +} + +.message { + display: flex; + flex-direction: column; + + > h2 { + margin-top: 10px; + } +} diff --git a/electron/src/components/missing-device/missing-device.component.ts b/electron/src/components/missing-device/missing-device.component.ts new file mode 100644 index 00000000..14a9ed77 --- /dev/null +++ b/electron/src/components/missing-device/missing-device.component.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; + +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/operator/distinctUntilChanged'; +import 'rxjs/add/operator/ignoreElements'; +import 'rxjs/add/operator/takeWhile'; + +import { UhkDeviceService } from './../../services/uhk-device.service'; + +@Component({ + selector: 'missing-device', + templateUrl: 'missing-device.component.html', + styleUrls: ['missing-device.component.scss'] +}) +export class MissingDeviceComponent { + + constructor(uhkDevice: UhkDeviceService, router: Router) { + uhkDevice.isConnected() + .distinctUntilChanged() + .takeWhile(connected => !connected) + .ignoreElements() + .subscribe({ + complete: () => { + router.navigate(['/']); + } + }); + } +} diff --git a/electron/src/index.html b/electron/src/index.html index 9196967c..e9fa1032 100644 --- a/electron/src/index.html +++ b/electron/src/index.html @@ -27,7 +27,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= - + diff --git a/electron/src/main-app/main-app.component.ts b/electron/src/main-app/main-app.component.ts index 46983280..8ab63457 100644 --- a/electron/src/main-app/main-app.component.ts +++ b/electron/src/main-app/main-app.component.ts @@ -1,7 +1,12 @@ import { Component, ViewEncapsulation, HostListener } from '@angular/core'; +import { Router } from '@angular/router'; import { Store } from '@ngrx/store'; +import 'rxjs/add/operator/distinctUntilChanged'; +import 'rxjs/add/operator/ignoreElements'; +import 'rxjs/add/operator/takeWhile'; + import { AppState } from '../shared/store'; import { getUserConfiguration } from '../shared/store/reducers/user-configuration'; @@ -17,7 +22,17 @@ import { UhkDeviceService } from '../services/uhk-device.service'; }) export class MainAppComponent { - constructor(private uhkDevice: UhkDeviceService, private store: Store) { } + constructor(private uhkDevice: UhkDeviceService, private store: Store, router: Router) { + uhkDevice.isConnected() + .distinctUntilChanged() + .takeWhile(connected => connected) + .ignoreElements() + .subscribe({ + complete: () => { + router.navigate(['/detection']); + } + }); + } @HostListener('window:keydown.control.o', ['$event']) onCtrlO(event: KeyboardEvent): void { diff --git a/electron/src/main-app/main-app.routes.ts b/electron/src/main-app/main-app.routes.ts index 3decdd8b..3ebb976a 100644 --- a/electron/src/main-app/main-app.routes.ts +++ b/electron/src/main-app/main-app.routes.ts @@ -1,18 +1,13 @@ -import { ModuleWithProviders } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; +import { Routes } from '@angular/router'; import { addOnRoutes } from '../shared/components/add-on'; import { keymapRoutes } from '../components/keymap'; import { macroRoutes } from '../shared/components/macro'; import { settingsRoutes } from '../shared/components/settings'; -const appRoutes: Routes = [ +export const mainAppRoutes: Routes = [ ...keymapRoutes, ...macroRoutes, ...addOnRoutes, ...settingsRoutes ]; - -export const appRoutingProviders: any[] = [ ]; - -export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes, { useHash: true }); diff --git a/electron/src/services/uhk-connected.guard.ts b/electron/src/services/uhk-connected.guard.ts new file mode 100644 index 00000000..17b7887a --- /dev/null +++ b/electron/src/services/uhk-connected.guard.ts @@ -0,0 +1,24 @@ +import { CanActivate, Router } from '@angular/router'; +import { Injectable } from '@angular/core'; + +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/first'; + +import { UhkDeviceService } from './uhk-device.service'; + +@Injectable() +export class UHkConnectedGuard implements CanActivate { + + constructor(private uhkDevice: UhkDeviceService, private router: Router) { } + + canActivate(): Observable { + return this.uhkDevice.isConnected() + .first() + .do(connected => { + if (!connected) { + return this.router.navigate(['/detection']); + } + }); + } +} diff --git a/electron/src/services/uhk-device.service.ts b/electron/src/services/uhk-device.service.ts index da996fc2..fb3422bd 100644 --- a/electron/src/services/uhk-device.service.ts +++ b/electron/src/services/uhk-device.service.ts @@ -1,5 +1,6 @@ -import { Injectable } from '@angular/core'; +import { Injectable, OnDestroy, NgZone } from '@angular/core'; import { Observable } from 'rxjs/Observable'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { Observer } from 'rxjs/Observer'; import { ConnectableObservable } from 'rxjs/observable/ConnectableObservable'; import { Subject } from 'rxjs/Subject'; @@ -14,7 +15,7 @@ import 'rxjs/add/operator/concatMap'; import 'rxjs/add/operator/mergeMap'; import 'rxjs/add/operator/publish'; -import { Device, Interface, InEndpoint, OutEndpoint, findByIds } from 'usb'; +import { Device, Interface, InEndpoint, OutEndpoint, findByIds, on } from 'usb'; import { Layer } from '../shared/config-serializer/config-items/Layer'; import { UhkBuffer } from '../shared/config-serializer/UhkBuffer'; @@ -34,28 +35,38 @@ interface SenderMessage { }; @Injectable() -export class UhkDeviceService { +export class UhkDeviceService implements OnDestroy { private device: Device; - private connected: boolean; + private connected$: BehaviorSubject; private messageIn$: Observable; private messageOut$: Subject; private outSubscription: Subscription; - constructor() { + constructor(zone: NgZone) { this.messageOut$ = new Subject(); + this.connected$ = new BehaviorSubject(false); this.connect(); + + // The change detection doesn't work properly if the callbacks are called outside Angular Zone + on('attach', (device: Device) => zone.run(() => this.onDeviceAttach(device))); + on('detach', (device: Device) => zone.run(() => this.onDeviceDetach(device))); + } + + ngOnDestroy() { + this.disconnect(); + this.connected$.unsubscribe(); } connect(): void { - if (this.connected) { + if (this.connected$.getValue()) { return; } this.device = findByIds(vendorId, productId); if (!this.device) { - throw new Error('UhkDevice not found.'); + return; } this.device.open(); @@ -103,18 +114,20 @@ export class UhkDeviceService { }).publish(); this.outSubscription = outSending.connect(); - this.connected = true; + this.connected$.next(true); } disconnect() { - if (!this.connected) { + if (!this.connected$.getValue()) { return; } this.outSubscription.unsubscribe(); this.messageIn$ = undefined; - this.device.interface(0).release(); - this.device.close(); - this.connected = false; + this.connected$.next(false); + } + + isConnected(): Observable { + return this.connected$.asObservable(); } sendConfig(configBuffer: Buffer): Observable { @@ -159,4 +172,22 @@ export class UhkDeviceService { }); } + onDeviceAttach(device: Device) { + if (device.deviceDescriptor.idVendor !== vendorId || device.deviceDescriptor.idProduct !== productId) { + return; + } + if (!this.connected$.getValue()) { + this.connect(); + } + } + + onDeviceDetach(device: Device) { + if (device.deviceDescriptor.idVendor !== vendorId || device.deviceDescriptor.idProduct !== productId) { + return; + } + if (this.connected$.getValue()) { + this.disconnect(); + } + } + } diff --git a/shared/src/main-app/main-app.component.scss b/shared/src/main-app/main-app.component.scss index 84412b34..afd48d6e 100644 --- a/shared/src/main-app/main-app.component.scss +++ b/shared/src/main-app/main-app.component.scss @@ -1,13 +1,11 @@ main-app { display: block; - min-height: 100vh; - width: 100%; overflow: hidden; position: relative; +} - .select2-container--default .select2-selection--single .select2-selection__rendered { - line-height: 26px; - } +.select2-container--default .select2-selection--single .select2-selection__rendered { + line-height: 26px; } .main-content { diff --git a/web/src/main-app/main-app.component.scss b/web/src/main-app/main-app.component.scss index 40dfa9a8..9f6eda12 100644 --- a/web/src/main-app/main-app.component.scss +++ b/web/src/main-app/main-app.component.scss @@ -30,3 +30,8 @@ text-shadow: 0 0 5px #444; } } + +main-app { + min-height: 100vh; + width: 100%; +}