From d7bdc15e499256dd7256623f3c66e72535da6bcd Mon Sep 17 00:00:00 2001 From: waterplea Date: Sat, 26 Mar 2022 16:24:34 +0300 Subject: [PATCH] refactor: decompose app component --- .../src/pages/list/item/item.component.html | 4 +- .../src/pages/list/item/item.component.scss | 4 - .../src/pages/list/item/item.component.ts | 1 - .../pages/show/package/package.component.html | 4 +- .../pages/show/package/package.component.scss | 1 - frontend/projects/shared/styles/shared.scss | 4 + .../projects/ui/src/app/app.component.html | 289 +----------------- .../projects/ui/src/app/app.component.scss | 10 +- frontend/projects/ui/src/app/app.component.ts | 207 ++----------- frontend/projects/ui/src/app/app.module.ts | 15 +- .../src/app/app/footer/footer.component.html | 16 + .../src/app/app/footer/footer.component.scss | 9 + .../ui/src/app/app/footer/footer.component.ts | 36 +++ .../ui/src/app/app/footer/footer.module.ts | 12 + .../ui/src/app/app/menu/menu.component.html | 60 ++++ .../ui/src/app/app/menu/menu.component.scss | 47 +++ .../ui/src/app/app/menu/menu.component.ts | 93 ++++++ .../ui/src/app/app/menu/menu.module.ts | 14 + .../app/preloader/preloader.component.html | 71 +++++ .../app/app/preloader/preloader.component.ts | 91 ++++++ .../src/app/app/preloader/preloader.module.ts | 14 + .../ui/src/app/app/snek/snek.directive.ts | 88 ++++++ .../ui/src/app/app/snek/snek.module.ts | 11 + .../ui/src/app/modals/snake/snake.module.ts | 3 +- .../app-show-dependencies.component.html | 2 +- .../app-show-dependencies.component.scss | 4 - .../app-show-header.component.html | 5 +- .../app-show-header.component.scss | 4 - .../marketplace-list-content.component.html | 2 +- .../marketplace-list-content.component.scss | 1 - .../marketplace-list/marketplace-list.page.ts | 31 +- .../marketplace-show-dependent.component.html | 2 +- .../marketplace-show-dependent.component.scss | 1 - .../server-show/server-show.module.ts | 2 - .../app/services/patch-db/patch-db.service.ts | 16 +- .../projects/ui/src/app/util/animations.ts | 17 ++ 36 files changed, 666 insertions(+), 525 deletions(-) delete mode 100644 frontend/projects/marketplace/src/pages/list/item/item.component.scss create mode 100644 frontend/projects/ui/src/app/app/footer/footer.component.html create mode 100644 frontend/projects/ui/src/app/app/footer/footer.component.scss create mode 100644 frontend/projects/ui/src/app/app/footer/footer.component.ts create mode 100644 frontend/projects/ui/src/app/app/footer/footer.module.ts create mode 100644 frontend/projects/ui/src/app/app/menu/menu.component.html create mode 100644 frontend/projects/ui/src/app/app/menu/menu.component.scss create mode 100644 frontend/projects/ui/src/app/app/menu/menu.component.ts create mode 100644 frontend/projects/ui/src/app/app/menu/menu.module.ts create mode 100644 frontend/projects/ui/src/app/app/preloader/preloader.component.html create mode 100644 frontend/projects/ui/src/app/app/preloader/preloader.component.ts create mode 100644 frontend/projects/ui/src/app/app/preloader/preloader.module.ts create mode 100644 frontend/projects/ui/src/app/app/snek/snek.directive.ts create mode 100644 frontend/projects/ui/src/app/app/snek/snek.module.ts create mode 100644 frontend/projects/ui/src/app/util/animations.ts diff --git a/frontend/projects/marketplace/src/pages/list/item/item.component.html b/frontend/projects/marketplace/src/pages/list/item/item.component.html index 47478892a..38ef01e1e 100644 --- a/frontend/projects/marketplace/src/pages/list/item/item.component.html +++ b/frontend/projects/marketplace/src/pages/list/item/item.component.html @@ -3,7 +3,9 @@ -

{{ pkg.manifest.title }}

+

+ {{ pkg.manifest.title }} +

{{ pkg.manifest.description.short }}

diff --git a/frontend/projects/marketplace/src/pages/list/item/item.component.scss b/frontend/projects/marketplace/src/pages/list/item/item.component.scss deleted file mode 100644 index 1b98d2b7f..000000000 --- a/frontend/projects/marketplace/src/pages/list/item/item.component.scss +++ /dev/null @@ -1,4 +0,0 @@ -.pkg-title { - font-family: 'Montserrat', sans-serif; - font-weight: bold; -} diff --git a/frontend/projects/marketplace/src/pages/list/item/item.component.ts b/frontend/projects/marketplace/src/pages/list/item/item.component.ts index 4997e8764..e026b6a15 100644 --- a/frontend/projects/marketplace/src/pages/list/item/item.component.ts +++ b/frontend/projects/marketplace/src/pages/list/item/item.component.ts @@ -5,7 +5,6 @@ import { MarketplacePkg } from '../../../types/marketplace-pkg' @Component({ selector: 'marketplace-item', templateUrl: 'item.component.html', - styleUrls: ['item.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) export class ItemComponent { diff --git a/frontend/projects/marketplace/src/pages/show/package/package.component.html b/frontend/projects/marketplace/src/pages/show/package/package.component.html index 568ba45be..2bbaab1ed 100644 --- a/frontend/projects/marketplace/src/pages/show/package/package.component.html +++ b/frontend/projects/marketplace/src/pages/show/package/package.component.html @@ -1,7 +1,7 @@ - -
+ +
-
- -
-
- - - - - - {{ page.title }} - - - - - {{ unreadCount }} - - - - -
-
- - - - Log Out - - - -
+
- +
- - - - - Downloading EOS: - {{ - ( - (100 * (osUpdateProgress?.downloaded || 1)) / - (osUpdateProgress?.size || 1) - ).toFixed(0) - }}% - -
- -
-
-
+ +
diff --git a/frontend/projects/ui/src/app/app.component.scss b/frontend/projects/ui/src/app/app.component.scss index 1f58d0b37..81dd85000 100644 --- a/frontend/projects/ui/src/app/app.component.scss +++ b/frontend/projects/ui/src/app/app.component.scss @@ -1,11 +1,7 @@ -.bold { - font-weight: bold; -} - -.dim { - color: var(--ion-color-dark-shade); +:host { + display: block; } ion-split-pane { --side-max-width: 280px; -} \ No newline at end of file +} diff --git a/frontend/projects/ui/src/app/app.component.ts b/frontend/projects/ui/src/app/app.component.ts index e2d45f1ba..6de8b5721 100644 --- a/frontend/projects/ui/src/app/app.component.ts +++ b/frontend/projects/ui/src/app/app.component.ts @@ -1,5 +1,5 @@ -import { Component, HostListener, Inject, NgZone } from '@angular/core' -import { Router, RoutesRecognized } from '@angular/router' +import { Component, HostListener, NgZone } from '@angular/core' +import { Router } from '@angular/router' import { AlertController, IonicSafeString, @@ -31,11 +31,10 @@ import { ConnectionService, } from './services/connection.service' import { ConfigService } from './services/config.service' -import { ServerStatus, UIData } from 'src/app/services/patch-db/data-model' +import { UIData } from 'src/app/services/patch-db/data-model' import { LocalStorageService } from './services/local-storage.service' import { EOSService } from './services/eos.service' import { OSWelcomePage } from './modals/os-welcome/os-welcome.page' -import { SnakePage } from './modals/snake/snake.page' @Component({ selector: 'app-root', @@ -43,83 +42,11 @@ import { SnakePage } from './modals/snake/snake.page' styleUrls: ['app.component.scss'], }) export class AppComponent { - code = { - s: false, - n: false, - e: false, - k: false, - unlocked: false, - } - - @HostListener('document:keydown.enter', ['$event']) - @debounce() - handleKeyboardEvent() { - const elems = document.getElementsByClassName('enter-click') - const elem = elems[elems.length - 1] as HTMLButtonElement - if (!elem || elem.classList.contains('no-click') || elem.disabled) return - if (elem) elem.click() - } - - @HostListener('document:keypress', ['$event']) - async keyPress(e: KeyboardEvent) { - if (e.repeat || this.code.unlocked) return - if (this.code[e.key] === false) { - this.code[e.key] = true - } - if ( - Object.entries(this.code) - .filter(([key, value]) => key.length === 1) - .map(([key, value]) => value) - .reduce((a, b) => a && b) - ) { - await this.openSnek() - } - } - - @HostListener('document:keyup', ['$event']) - keyUp(e: KeyboardEvent) { - if (this.code[e.key]) { - this.code[e.key] = false - } - } - - ServerStatus = ServerStatus showMenu = false - selectedIndex = 0 offlineToast: HTMLIonToastElement updateToast: HTMLIonToastElement notificationToast: HTMLIonToastElement - serverName: string - unreadCount: number subscriptions: Subscription[] = [] - osUpdateProgress: { size: number; downloaded: number } - appPages = [ - { - title: 'Services', - url: '/services', - icon: 'grid-outline', - }, - { - title: 'Embassy', - url: '/embassy', - icon: 'cube-outline', - }, - { - title: 'Marketplace', - url: '/marketplace', - icon: 'storefront-outline', - }, - { - title: 'Notifications', - url: '/notifications', - icon: 'notifications-outline', - }, - { - title: 'Developer Tools', - url: '/developer', - icon: 'hammer-outline', - }, - ] constructor( private readonly storage: Storage, @@ -135,14 +62,29 @@ export class AppComponent { private readonly errToast: ErrorToastService, private readonly config: ConfigService, private readonly zone: NgZone, - public readonly splitPane: SplitPaneTracker, - public readonly patch: PatchDbService, - public readonly localStorageService: LocalStorageService, - public readonly eosService: EOSService, + private readonly splitPane: SplitPaneTracker, + private readonly patch: PatchDbService, + private readonly localStorageService: LocalStorageService, + private readonly eosService: EOSService, ) { this.init() } + @HostListener('document:keydown.enter', ['$event']) + @debounce() + handleKeyboardEvent() { + const elems = document.getElementsByClassName('enter-click') + const elem = elems[elems.length - 1] as HTMLButtonElement + + if (elem && !elem.classList.contains('no-click') && !elem.disabled) { + elem.click() + } + } + + splitPaneVisible({ detail }: any) { + this.splitPane.sidebarOpen$.next(detail.visible) + } + async init() { await this.storage.create() await this.authService.init() @@ -167,8 +109,6 @@ export class AppComponent { ...this.connectionService.start(), // watch connection to display connectivity issues this.watchConnection(), - // watch router to highlight selected menu item - this.watchRouter(), ]) this.patch @@ -186,8 +126,6 @@ export class AppComponent { this.subscriptions = this.subscriptions.concat([ // watch status to present toast for updated state this.watchStatus(), - // watch update-progress to present progress bar when server is updating - this.watchUpdateProgress(), // watch version to refresh browser window this.watchVersion(), // watch unread notification count to display toast @@ -212,40 +150,6 @@ export class AppComponent { }) } - async goToWebsite(): Promise { - let url: string - if (this.config.isTor()) { - url = - 'http://privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion' - } else { - url = 'https://start9.com' - } - window.open(url, '_blank', 'noreferrer') - } - - async presentAlertLogout() { - const alert = await this.alertCtrl.create({ - header: 'Caution', - message: - 'Do you know your password? If you log out and forget your password, you may permanently lose access to your Embassy.', - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Logout', - cssClass: 'enter-click', - handler: () => { - this.logout() - }, - }, - ], - }) - - await alert.present() - } - private checkForEosUpdate(ui: UIData): void { if (ui['auto-check-updates'] !== false) { this.eosService.getEOS() @@ -271,48 +175,6 @@ export class AppComponent { } } - async openSnek() { - this.code.unlocked = true - const modal = await this.modalCtrl.create({ - component: SnakePage, - cssClass: 'snake-modal', - backdropDismiss: false, - }) - - modal.onDidDismiss().then(async ret => { - this.code.unlocked = false - if ( - ret.data.highScore && - (ret.data.highScore > - this.patch.getData().ui.gaming?.snake?.['high-score'] || - !this.patch.getData().ui.gaming?.snake?.['high-score']) - ) { - const loader = await this.loadingCtrl.create({ - spinner: 'lines', - cssClass: 'loader', - message: 'Saving High Score...', - }) - await loader.present() - try { - await this.embassyApi.setDbValue({ - pointer: '/gaming', - value: { snake: { 'high-score': ret.data.highScore } }, - }) - } catch (e) { - this.errToast.present(e) - } finally { - this.loadingCtrl.dismiss() - } - } - }) - modal.present() - } - // should wipe cache independant of actual BE logout - private async logout() { - this.embassyApi.logout({}) - this.authService.setUnverified() - } - private watchConnection(): Subscription { return this.connectionService .watchFailure$() @@ -344,17 +206,6 @@ export class AppComponent { }) } - private watchRouter(): Subscription { - return this.router.events - .pipe(filter((e: RoutesRecognized) => !!e.urlAfterRedirects)) - .subscribe(e => { - const appPageIndex = this.appPages.findIndex(appPage => - e.urlAfterRedirects.startsWith(appPage.url), - ) - if (appPageIndex > -1) this.selectedIndex = appPageIndex - }) - } - private watchStatus(): Subscription { return this.patch .watch$('server-info', 'status-info', 'updated') @@ -364,15 +215,6 @@ export class AppComponent { } }) } - m - - private watchUpdateProgress(): Subscription { - return this.patch - .watch$('server-info', 'status-info', 'update-progress') - .subscribe(progress => { - this.osUpdateProgress = progress - }) - } private watchVersion(): Subscription { return this.patch.watch$('server-info', 'version').subscribe(version => { @@ -387,7 +229,6 @@ export class AppComponent { return this.patch .watch$('server-info', 'unread-notification-count') .subscribe(count => { - this.unreadCount = count if (previous !== undefined && count > previous) this.presentToastNotifications() previous = count @@ -530,8 +371,4 @@ export class AppComponent { loader.dismiss() } } - - splitPaneVisible(e: any) { - this.splitPane.sidebarOpen$.next(e.detail.visible) - } } diff --git a/frontend/projects/ui/src/app/app.module.ts b/frontend/projects/ui/src/app/app.module.ts index b063ad1c0..d6d3e0eb3 100644 --- a/frontend/projects/ui/src/app/app.module.ts +++ b/frontend/projects/ui/src/app/app.module.ts @@ -1,5 +1,5 @@ -import { NgModule, CUSTOM_ELEMENTS_SCHEMA, ErrorHandler } from '@angular/core' -import { BrowserModule } from '@angular/platform-browser' +import { NgModule, ErrorHandler } from '@angular/core' +import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { RouteReuseStrategy } from '@angular/router' import { IonicModule, IonicRouteStrategy, IonNav } from '@ionic/angular' import { Drivers } from '@ionic/storage' @@ -10,7 +10,6 @@ import { AppRoutingModule } from './app-routing.module' import { ApiService } from './services/api/embassy-api.service' import { PatchDbServiceFactory } from './services/patch-db/patch-db.factory' import { ConfigService } from './services/config.service' -import { QrCodeModule } from 'ng-qrcode' import { OSWelcomePageModule } from './modals/os-welcome/os-welcome.module' import { PatchDbService } from './services/patch-db/patch-db.service' import { LocalStorageBootstrap } from './services/patch-db/local-storage-bootstrap' @@ -27,6 +26,9 @@ import { WorkspaceConfig, } from '@start9labs/shared' import { MarketplaceModule } from './marketplace.module' +import { PreloaderModule } from './app/preloader/preloader.module' +import { FooterModule } from './app/footer/footer.module' +import { MenuModule } from './app/menu/menu.module' const { useMocks } = require('../../../../config.json') as WorkspaceConfig @@ -35,7 +37,7 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig entryComponents: [], imports: [ HttpClientModule, - BrowserModule, + BrowserAnimationsModule, IonicModule.forRoot({ mode: 'md', }), @@ -46,7 +48,9 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig name: '_embassystorage', driverOrder: [Drivers.LocalStorage, Drivers.IndexedDB], }), - QrCodeModule, + MenuModule, + PreloaderModule, + FooterModule, OSWelcomePageModule, MarkdownModule, GenericInputComponentModule, @@ -82,6 +86,5 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig }, ], bootstrap: [AppComponent], - schemas: [CUSTOM_ELEMENTS_SCHEMA], }) export class AppModule {} diff --git a/frontend/projects/ui/src/app/app/footer/footer.component.html b/frontend/projects/ui/src/app/app/footer/footer.component.html new file mode 100644 index 000000000..2e8874613 --- /dev/null +++ b/frontend/projects/ui/src/app/app/footer/footer.component.html @@ -0,0 +1,16 @@ + + + + Downloading EOS: {{ getProgress(progress) }}% + + + + diff --git a/frontend/projects/ui/src/app/app/footer/footer.component.scss b/frontend/projects/ui/src/app/app/footer/footer.component.scss new file mode 100644 index 000000000..cd737e850 --- /dev/null +++ b/frontend/projects/ui/src/app/app/footer/footer.component.scss @@ -0,0 +1,9 @@ +.list { + box-shadow: inset 0 1px var(--ion-color-dark); + box-sizing: border-box; +} + +.progress { + width: auto; + margin: 0 16px 16px 16px; +} diff --git a/frontend/projects/ui/src/app/app/footer/footer.component.ts b/frontend/projects/ui/src/app/app/footer/footer.component.ts new file mode 100644 index 000000000..7ac3a581b --- /dev/null +++ b/frontend/projects/ui/src/app/app/footer/footer.component.ts @@ -0,0 +1,36 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' + +import { heightCollapse } from '../../util/animations' +import { PatchDbService } from '../../services/patch-db/patch-db.service' +import { map } from 'rxjs/operators' +import { ServerInfo } from '../../services/patch-db/data-model' + +@Component({ + selector: 'footer[appFooter]', + templateUrl: 'footer.component.html', + styleUrls: ['footer.component.scss'], + animations: [heightCollapse], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FooterComponent { + readonly progress$ = this.patch + .watch$('server-info', 'status-info', 'update-progress') + .pipe(map(a => a && { ...a })) + + readonly animation = { + value: '', + params: { + duration: 1000, + delay: 50, + }, + } + + constructor(private readonly patch: PatchDbService) {} + + getProgress({ + downloaded, + size, + }: ServerInfo['status-info']['update-progress']): number { + return Math.round((100 * (downloaded || 1)) / (size || 1)) + } +} diff --git a/frontend/projects/ui/src/app/app/footer/footer.module.ts b/frontend/projects/ui/src/app/app/footer/footer.module.ts new file mode 100644 index 000000000..c76e4b26e --- /dev/null +++ b/frontend/projects/ui/src/app/app/footer/footer.module.ts @@ -0,0 +1,12 @@ +import { CommonModule } from '@angular/common' +import { NgModule } from '@angular/core' +import { IonicModule } from '@ionic/angular' + +import { FooterComponent } from './footer.component' + +@NgModule({ + imports: [CommonModule, IonicModule], + declarations: [FooterComponent], + exports: [FooterComponent], +}) +export class FooterModule {} diff --git a/frontend/projects/ui/src/app/app/menu/menu.component.html b/frontend/projects/ui/src/app/app/menu/menu.component.html new file mode 100644 index 000000000..a68a3c4e5 --- /dev/null +++ b/frontend/projects/ui/src/app/app/menu/menu.component.html @@ -0,0 +1,60 @@ + +
+ + + + + + {{ page.title }} + + + + {{ count }} + + + + +Play Snek +
+
+ + + + Log Out + + + +
diff --git a/frontend/projects/ui/src/app/app/menu/menu.component.scss b/frontend/projects/ui/src/app/app/menu/menu.component.scss new file mode 100644 index 000000000..799b220ba --- /dev/null +++ b/frontend/projects/ui/src/app/app/menu/menu.component.scss @@ -0,0 +1,47 @@ +:host { + display: block; +} + +.logo { + display: block; + width: 50%; + margin: 0 auto; +} + +.menu { + padding: 30px 0; +} + +.icon { + margin-left: 10px; +} + +.label { + color: var(--ion-color-dark-shade); + + &_selected { + color: #fff; + font-weight: bold; + } +} + +.badge { + margin-right: 3%; +} + +.snek { + position: absolute; + bottom: 90px; + left: 20px; + width: 20px; + cursor: pointer; +} + +.bottom { + position: absolute; + bottom: 0; + right: 0; + width: 100%; + height: 75px; + text-align: center; +} diff --git a/frontend/projects/ui/src/app/app/menu/menu.component.ts b/frontend/projects/ui/src/app/app/menu/menu.component.ts new file mode 100644 index 000000000..c185c39fb --- /dev/null +++ b/frontend/projects/ui/src/app/app/menu/menu.component.ts @@ -0,0 +1,93 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core' +import { AlertController } from '@ionic/angular' + +import { ConfigService } from '../../services/config.service' +import { LocalStorageService } from '../../services/local-storage.service' +import { EOSService } from '../../services/eos.service' +import { ApiService } from '../../services/api/embassy-api.service' +import { AuthService } from '../../services/auth.service' +import { PatchDbService } from '../../services/patch-db/patch-db.service' + +@Component({ + selector: 'app-menu', + templateUrl: 'menu.component.html', + styleUrls: ['menu.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MenuComponent { + readonly pages = [ + { + title: 'Services', + url: '/services', + icon: 'grid-outline', + }, + { + title: 'Embassy', + url: '/embassy', + icon: 'cube-outline', + }, + { + title: 'Marketplace', + url: '/marketplace', + icon: 'storefront-outline', + }, + { + title: 'Notifications', + url: '/notifications', + icon: 'notifications-outline', + }, + { + title: 'Developer Tools', + url: '/developer', + icon: 'hammer-outline', + }, + ] + + readonly notification$ = this.patch.watch$( + 'server-info', + 'unread-notification-count', + ) + + constructor( + private readonly config: ConfigService, + private readonly alertCtrl: AlertController, + private readonly embassyApi: ApiService, + private readonly authService: AuthService, + private readonly patch: PatchDbService, + public readonly localStorageService: LocalStorageService, + public readonly eosService: EOSService, + ) {} + + get href(): string { + return this.config.isTor() + ? 'http://privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion' + : 'https://start9.com' + } + + async presentAlertLogout() { + const alert = await this.alertCtrl.create({ + header: 'Caution', + message: + 'Do you know your password? If you log out and forget your password, you may permanently lose access to your Embassy.', + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'Logout', + cssClass: 'enter-click', + handler: () => this.logout(), + }, + ], + }) + + await alert.present() + } + + // should wipe cache independent of actual BE logout + private logout() { + this.embassyApi.logout({}) + this.authService.setUnverified() + } +} diff --git a/frontend/projects/ui/src/app/app/menu/menu.module.ts b/frontend/projects/ui/src/app/app/menu/menu.module.ts new file mode 100644 index 000000000..baa2bedbb --- /dev/null +++ b/frontend/projects/ui/src/app/app/menu/menu.module.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common' +import { NgModule } from '@angular/core' +import { RouterModule } from '@angular/router' +import { IonicModule } from '@ionic/angular' + +import { MenuComponent } from './menu.component' +import { SnekModule } from '../snek/snek.module' + +@NgModule({ + imports: [CommonModule, IonicModule, RouterModule, SnekModule], + declarations: [MenuComponent], + exports: [MenuComponent], +}) +export class MenuModule {} diff --git a/frontend/projects/ui/src/app/app/preloader/preloader.component.html b/frontend/projects/ui/src/app/app/preloader/preloader.component.html new file mode 100644 index 000000000..5662f3e76 --- /dev/null +++ b/frontend/projects/ui/src/app/app/preloader/preloader.component.html @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +load bold font + + + + + + + +

a

+

a

+

a

+

a

+

a

+

a

+ + + + + + + \ No newline at end of file diff --git a/frontend/projects/ui/src/app/app/preloader/preloader.component.ts b/frontend/projects/ui/src/app/app/preloader/preloader.component.ts new file mode 100644 index 000000000..062a890fa --- /dev/null +++ b/frontend/projects/ui/src/app/app/preloader/preloader.component.ts @@ -0,0 +1,91 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core' + +// TODO: Turn into DI token if this is needed someplace else too +const ICONS = [ + 'add', + 'alert-outline', + 'alert-circle-outline', + 'aperture-outline', + 'arrow-back', + 'arrow-up', + 'briefcase-outline', + 'bookmark-outline', + 'cellular-outline', + 'chatbubbles-outline', + 'checkmark', + 'chevron-down', + 'chevron-up', + 'chevron-forward', + 'close', + 'cloud-outline', + 'cloud-done-outline', + 'cloud-download-outline', + 'cloud-offline-outline', + 'cloud-upload-outline', + 'code-outline', + 'cog-outline', + 'color-wand-outline', + 'construct-outline', + 'copy-outline', + 'cube-outline', + 'desktop-outline', + 'download-outline', + 'earth-outline', + 'ellipsis-horizontal-outline', + 'eye-off-outline', + 'eye-outline', + 'file-tray-stacked-outline', + 'finger-print-outline', + 'flash-outline', + 'folder-open-outline', + 'grid-outline', + 'help-circle-outline', + 'hammer-outline', + 'home-outline', + 'information-circle-outline', + 'key-outline', + 'list-outline', + 'lock-closed-outline', + 'logo-bitcoin', + 'mail-outline', + 'map-outline', + 'medkit-outline', + 'newspaper-outline', + 'notifications-outline', + 'open-outline', + 'options-outline', + 'pencil', + 'phone-portrait-outline', + 'play-circle-outline', + 'power', + 'pulse', + 'qr-code-outline', + 'receipt-outline', + 'refresh', + 'reload', + 'remove', + 'remove-circle-outline', + 'remove-outline', + 'reorder-three', + 'rocket-outline', + 'save-outline', + 'shield-checkmark-outline', + 'stop-outline', + 'storefront-outline', + 'swap-vertical', + 'terminal-outline', + 'trash', + 'trash-outline', + 'warning-outline', + 'wifi', +] + +@Component({ + selector: 'section[appPreloader]', + templateUrl: 'preloader.component.html', + styles: [':host { display: none }'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PreloaderComponent { + readonly icons = ICONS +} diff --git a/frontend/projects/ui/src/app/app/preloader/preloader.module.ts b/frontend/projects/ui/src/app/app/preloader/preloader.module.ts new file mode 100644 index 000000000..c5a7bcf47 --- /dev/null +++ b/frontend/projects/ui/src/app/app/preloader/preloader.module.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common' +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core' +import { IonicModule } from '@ionic/angular' +import { QrCodeModule } from 'ng-qrcode' + +import { PreloaderComponent } from './preloader.component' + +@NgModule({ + imports: [CommonModule, IonicModule, QrCodeModule], + declarations: [PreloaderComponent], + exports: [PreloaderComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class PreloaderModule {} diff --git a/frontend/projects/ui/src/app/app/snek/snek.directive.ts b/frontend/projects/ui/src/app/app/snek/snek.directive.ts new file mode 100644 index 000000000..1f1defe5d --- /dev/null +++ b/frontend/projects/ui/src/app/app/snek/snek.directive.ts @@ -0,0 +1,88 @@ +import { Directive, HostListener } from '@angular/core' +import { LoadingController, ModalController } from '@ionic/angular' +import { ErrorToastService } from '@start9labs/shared' + +import { SnakePage } from '../../modals/snake/snake.page' +import { PatchDbService } from '../../services/patch-db/patch-db.service' +import { ApiService } from '../../services/api/embassy-api.service' + +const SNEK = ['s', 'n', 'e', 'k'] + +@Directive({ + selector: 'img[appSnek]', +}) +export class SnekDirective { + private readonly code = new Map([ + ...SNEK.map<[string, boolean]>(char => [char, false]), + ['unlocked', false], + ]) + + constructor( + private readonly modalCtrl: ModalController, + private readonly loadingCtrl: LoadingController, + private readonly errToast: ErrorToastService, + private readonly embassyApi: ApiService, + private readonly patch: PatchDbService, + ) {} + + @HostListener('document:keyup', ['$event.key']) + onKeyUp(key: string) { + this.code.set(key, false) + } + + @HostListener('document:keypress', ['$event']) + async onKeyPress({ repeat, key }: KeyboardEvent) { + if (repeat || this.code.get('unlocked')) return + + this.code.set(key, true) + + if (SNEK.every(char => this.code.get(char))) { + await this.openSnek() + } + } + + @HostListener('click') + async onClick() { + await this.openSnek() + } + + private async openSnek() { + this.code.set('unlocked', true) + + const modal = await this.modalCtrl.create({ + component: SnakePage, + cssClass: 'snake-modal', + backdropDismiss: false, + }) + + modal.onDidDismiss().then(async ({ data }) => { + this.code.set('unlocked', false) + + const highScore = + this.patch.getData().ui.gaming?.snake?.['high-score'] || 0 + + if (data?.highScore > highScore) { + const loader = await this.loadingCtrl.create({ + spinner: 'lines', + cssClass: 'loader', + message: 'Saving High Score...', + }) + + await loader.present() + + try { + await this.embassyApi.setDbValue({ + pointer: '/gaming', + value: { snake: { 'high-score': data.highScore } }, + }) + } catch (e) { + this.errToast.present(e) + } finally { + this.loadingCtrl.dismiss() + } + } + }) + + modal.present() + } +} diff --git a/frontend/projects/ui/src/app/app/snek/snek.module.ts b/frontend/projects/ui/src/app/app/snek/snek.module.ts new file mode 100644 index 000000000..f01c59c35 --- /dev/null +++ b/frontend/projects/ui/src/app/app/snek/snek.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core' + +import { SnakePageModule } from '../../modals/snake/snake.module' +import { SnekDirective } from './snek.directive' + +@NgModule({ + imports: [SnakePageModule], + declarations: [SnekDirective], + exports: [SnekDirective], +}) +export class SnekModule {} diff --git a/frontend/projects/ui/src/app/modals/snake/snake.module.ts b/frontend/projects/ui/src/app/modals/snake/snake.module.ts index 071db414e..cc9c937d0 100644 --- a/frontend/projects/ui/src/app/modals/snake/snake.module.ts +++ b/frontend/projects/ui/src/app/modals/snake/snake.module.ts @@ -1,11 +1,12 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' + import { SnakePage } from './snake.page' @NgModule({ - declarations: [SnakePage], imports: [CommonModule, IonicModule], + declarations: [SnakePage], exports: [SnakePage], }) export class SnakePageModule {} diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.html b/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.html index 13e79486e..e9f7b97d7 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.html +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.html @@ -5,7 +5,7 @@ -

+

-

+

{{ pkg.manifest.title }}

{{ pkg.manifest.version | displayEmver }}

diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-header/app-show-header.component.scss b/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-header/app-show-header.component.scss index fff277abd..0dd950e80 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-header/app-show-header.component.scss +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-header/app-show-header.component.scss @@ -1,7 +1,3 @@ -.name { - font-family: 'Montserrat', sans-serif; -} - .less-large { font-size: 18px !important; } diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-content/marketplace-list-content.component.html b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-content/marketplace-list-content.component.html index a9acc2d5c..8d8555086 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-content/marketplace-list-content.component.html +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-content/marketplace-list-content.component.html @@ -1,4 +1,4 @@ -

{{ name }}

+

{{ name }}

diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-content/marketplace-list-content.component.scss b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-content/marketplace-list-content.component.scss index 61046d840..9bad1b183 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-content/marketplace-list-content.component.scss +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-content/marketplace-list-content.component.scss @@ -1,5 +1,4 @@ .heading { - font-family: 'Montserrat', sans-serif; font-size: 42px; margin: 32px 0; } diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts index dc2ecb8ff..0ef493279 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core' -import { defer, Observable } from 'rxjs' +import { Observable } from 'rxjs' import { filter, first, map, startWith, switchMapTo, tap } from 'rxjs/operators' import { exists, isEmptyObject } from '@start9labs/shared' import { @@ -16,14 +16,13 @@ import { spreadProgress } from '../utils/spread-progress' templateUrl: './marketplace-list.page.html', }) export class MarketplaceListPage { - readonly localPkgs$: Observable> = defer( - () => this.patch.watch$('package-data'), - ).pipe( - filter(data => exists(data) && !isEmptyObject(data)), - tap(pkgs => Object.values(pkgs).forEach(spreadProgress)), - map(pkgs => ({ ...pkgs })), - startWith({}), - ) + readonly localPkgs$: Observable> = this.patch + .watch$('package-data') + .pipe( + filter(data => exists(data) && !isEmptyObject(data)), + tap(pkgs => Object.values(pkgs).forEach(spreadProgress)), + startWith({}), + ) readonly categories$ = this.marketplaceService .getCategories() @@ -31,13 +30,13 @@ export class MarketplaceListPage { map(categories => new Set(['featured', 'updates', ...categories, 'all'])), ) - readonly pkgs$: Observable = defer(() => - this.patch.watch$('server-info'), - ).pipe( - filter(data => exists(data) && !isEmptyObject(data)), - first(), - switchMapTo(this.marketplaceService.getPackages()), - ) + readonly pkgs$: Observable = this.patch + .watch$('server-info') + .pipe( + filter(data => exists(data) && !isEmptyObject(data)), + first(), + switchMapTo(this.marketplaceService.getPackages()), + ) readonly name$: Observable = this.marketplaceService .getMarketplace() diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.html b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.html index 6827c6337..a7e5f9cb6 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.html +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.html @@ -2,7 +2,7 @@

- + {{ title }}

diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.scss b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.scss index 83c4d2737..5fc4b4248 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.scss +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.scss @@ -5,7 +5,6 @@ .title { margin: 5px; - font-family: 'Montserrat', sans-serif; font-size: 18px; } diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.module.ts b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.module.ts index 2ec7b74d4..2efdac950 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.module.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.module.ts @@ -6,7 +6,6 @@ import { ServerShowPage } from './server-show.page' import { FormsModule } from '@angular/forms' import { TextSpinnerComponentModule } from '@start9labs/shared' import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' -import { SnakePageModule } from 'src/app/modals/snake/snake.module' const routes: Routes = [ { @@ -23,7 +22,6 @@ const routes: Routes = [ RouterModule.forChild(routes), TextSpinnerComponentModule, BadgeMenuComponentModule, - SnakePageModule, ], declarations: [ServerShowPage], }) diff --git a/frontend/projects/ui/src/app/services/patch-db/patch-db.service.ts b/frontend/projects/ui/src/app/services/patch-db/patch-db.service.ts index 9c6b0726a..7ece20432 100644 --- a/frontend/projects/ui/src/app/services/patch-db/patch-db.service.ts +++ b/frontend/projects/ui/src/app/services/patch-db/patch-db.service.ts @@ -7,6 +7,9 @@ import { debounceTime, finalize, mergeMap, + skip, + switchMap, + take, tap, withLatestFrom, } from 'rxjs/operators' @@ -14,6 +17,7 @@ import { isEmptyObject, pauseFor } from '@start9labs/shared' import { DataModel } from './data-model' import { ApiService } from '../api/embassy-api.service' import { AuthService } from '../auth.service' +import { patch } from '@start9labs/emver' export const PATCH_HTTP = new InjectionToken>('') export const PATCH_SOURCE = new InjectionToken>('') @@ -176,9 +180,19 @@ export class PatchDbService { // prettier-ignore watch$: Store['watch$'] = (...args: (string | number)[]): Observable => { + // TODO: refactor with a better solution to race condition const argsString = '/' + args.join('/') + const source$ = + this.patchDb?.store.watch$(...(args as [])) || + this.patchConnection$.pipe( + skip(1), + take(1), + switchMap(() => this.patchDb.store.watch$(...(args as []))), + ) + console.log('patchDB: WATCHING ', argsString) - return this.patchDb.store.watch$(...(args as [])).pipe( + + return source$.pipe( tap(data => console.log('patchDB: NEW VALUE', argsString, data)), catchError(e => { console.error('patchDB: WATCH ERROR', e) diff --git a/frontend/projects/ui/src/app/util/animations.ts b/frontend/projects/ui/src/app/util/animations.ts new file mode 100644 index 000000000..06191dd7a --- /dev/null +++ b/frontend/projects/ui/src/app/util/animations.ts @@ -0,0 +1,17 @@ +import { animate, style, transition, trigger } from '@angular/animations' + +const TRANSITION = '{{duration}}ms {{delay}}ms ease-in-out' +const DURATION = { params: { duration: 300, delay: 0 } } + +export const heightCollapse = trigger('heightCollapse', [ + transition( + ':enter', + [style({ height: 0 }), animate(TRANSITION, style({ height: '*' }))], + DURATION, + ), + transition( + ':leave', + [style({ height: '*' }), animate(TRANSITION, style({ height: 0 }))], + DURATION, + ), +])