refactor: decompose app component

This commit is contained in:
waterplea
2022-03-26 16:24:34 +03:00
committed by Lucy C
parent 50f14fe040
commit d7bdc15e49
36 changed files with 666 additions and 525 deletions

View File

@@ -1,300 +1,21 @@
<ion-app>
<ion-content>
<ion-split-pane
contentId="main-content"
[disabled]="!showMenu"
(ionSplitPaneVisible)="splitPaneVisible($event)"
contentId="main-content"
>
<ion-menu contentId="main-content" type="overlay">
<ion-content color="light" scrollY="false">
<div style="text-align: center" class="ion-padding">
<img
style="width: 45%; cursor: pointer"
src="assets/img/logo.png"
(click)="goToWebsite()"
/>
</div>
<div class="divider"></div>
<ion-item-group style="padding: 30px 0px">
<ion-menu-toggle
auto-hide="false"
*ngFor="let page of appPages; let i = index"
>
<ion-item
style="padding-left: 10px"
color="transparent"
button
(click)="selectedIndex = i"
routerDirection="root"
[routerLink]="[page.url]"
lines="none"
detail="false"
*ngIf="
page.url !== '/developer' ||
(localStorageService.showDevTools$ | async)
"
>
<ion-icon
slot="start"
[name]="page.icon"
[class]="selectedIndex === i ? 'bold' : 'dim'"
></ion-icon>
<ion-label
style="font-family: 'Montserrat'"
[class]="selectedIndex === i ? 'bold' : 'dim'"
>
{{ page.title }}
</ion-label>
<ng-container *ngIf="page.url === '/embassy'">
<ion-icon
*ngIf="eosService.updateAvailable$ | async"
color="success"
size="small"
name="rocket-outline"
></ion-icon>
</ng-container>
<ion-badge
*ngIf="page.url === '/notifications' && unreadCount"
color="danger"
style="margin-right: 3%"
>{{ unreadCount }}</ion-badge
>
</ion-item>
</ion-menu-toggle>
</ion-item-group>
<img
(click)="openSnek()"
style="
cursor: pointer;
position: absolute;
bottom: 90px;
left: 20px;
width: 20px;
"
src="assets/img/icons/snek.png"
/>
<div
style="
text-align: center;
height: 75px;
position: absolute;
bottom: 0;
right: 0;
width: 100%;
"
>
<div class="divider" style="margin-bottom: 10px"></div>
<ion-menu-toggle auto-hide="false">
<ion-item
button
lines="none"
style="
--background: transparent;
margin-bottom: 86px;
text-align: center;
"
fill="clear"
(click)="presentAlertLogout()"
>
<ion-label
><ion-text style="font-family: 'Montserrat'" color="dark">
Log Out
</ion-text></ion-label
>
</ion-item>
</ion-menu-toggle>
</div>
<app-menu></app-menu>
</ion-content>
</ion-menu>
<ion-router-outlet id="main-content"></ion-router-outlet>
</ion-split-pane>
<section id="preload" style="display: none">
<!-- 3rd party components -->
<qr-code value="hello"></qr-code>
<!-- Ionicons -->
<ion-icon name="add"></ion-icon>
<ion-icon name="alert-outline"></ion-icon>
<ion-icon name="alert-circle-outline"></ion-icon>
<ion-icon name="aperture-outline"></ion-icon>
<ion-icon name="arrow-back"></ion-icon>
<ion-icon name="arrow-forward"></ion-icon>
<ion-icon name="arrow-up"></ion-icon>
<ion-icon name="briefcase-outline"></ion-icon>
<ion-icon name="bookmark-outline"></ion-icon>
<ion-icon name="cellular-outline"></ion-icon>
<ion-icon name="chatbubbles-outline"></ion-icon>
<ion-icon name="checkmark"></ion-icon>
<ion-icon name="chevron-down"></ion-icon>
<ion-icon name="chevron-up"></ion-icon>
<!-- needed for detail="true" on ion-item button -->
<ion-icon name="chevron-forward"></ion-icon>
<ion-icon name="close"></ion-icon>
<ion-icon name="cloud-outline"></ion-icon>
<ion-icon name="cloud-done-outline"></ion-icon>
<ion-icon name="cloud-download-outline"></ion-icon>
<ion-icon name="cloud-offline-outline"></ion-icon>
<ion-icon name="cloud-upload-outline"></ion-icon>
<ion-icon name="code-outline"></ion-icon>
<ion-icon name="cog-outline"></ion-icon>
<ion-icon name="color-wand-outline"></ion-icon>
<ion-icon name="construct-outline"></ion-icon>
<ion-icon name="copy-outline"></ion-icon>
<ion-icon name="cube-outline"></ion-icon>
<ion-icon name="desktop-outline"></ion-icon>
<ion-icon name="download-outline"></ion-icon>
<ion-icon name="earth-outline"></ion-icon>
<ion-icon name="ellipsis-horizontal-outline"></ion-icon>
<ion-icon name="eye-off-outline"></ion-icon>
<ion-icon name="eye-outline"></ion-icon>
<ion-icon name="file-tray-stacked-outline"></ion-icon>
<ion-icon name="finger-print-outline"></ion-icon>
<ion-icon name="flash-outline"></ion-icon>
<ion-icon name="folder-open-outline"></ion-icon>
<ion-icon name="grid-outline"></ion-icon>
<ion-icon name="help-circle-outline"></ion-icon>
<ion-icon name="hammer-outline"></ion-icon>
<ion-icon name="home-outline"></ion-icon>
<ion-icon name="information-circle-outline"></ion-icon>
<ion-icon name="key-outline"></ion-icon>
<ion-icon name="list-outline"></ion-icon>
<ion-icon name="lock-closed-outline"></ion-icon>
<ion-icon name="logo-bitcoin"></ion-icon>
<ion-icon name="mail-outline"></ion-icon>
<ion-icon name="map-outline"></ion-icon>
<ion-icon name="medkit-outline"></ion-icon>
<ion-icon name="newspaper-outline"></ion-icon>
<ion-icon name="notifications-outline"></ion-icon>
<ion-icon name="open-outline"></ion-icon>
<ion-icon name="options-outline"></ion-icon>
<ion-icon name="pencil"></ion-icon>
<ion-icon name="phone-portrait-outline"></ion-icon>
<ion-icon name="play-circle-outline"></ion-icon>
<ion-icon name="power"></ion-icon>
<ion-icon name="pulse"></ion-icon>
<ion-icon name="qr-code-outline"></ion-icon>
<ion-icon name="receipt-outline"></ion-icon>
<ion-icon name="refresh"></ion-icon>
<ion-icon name="reload"></ion-icon>
<ion-icon name="remove"></ion-icon>
<ion-icon name="remove-circle-outline"></ion-icon>
<ion-icon name="remove-outline"></ion-icon>
<ion-icon name="reorder-three"></ion-icon>
<ion-icon name="rocket-outline"></ion-icon>
<ion-icon name="save-outline"></ion-icon>
<ion-icon name="shield-checkmark-outline"></ion-icon>
<ion-icon name="stop-outline"></ion-icon>
<ion-icon name="storefront-outline"></ion-icon>
<ion-icon name="swap-vertical"></ion-icon>
<ion-icon name="terminal-outline"></ion-icon>
<ion-icon name="trash"></ion-icon>
<ion-icon name="trash-outline"></ion-icon>
<ion-icon name="warning-outline"></ion-icon>
<ion-icon name="wifi"></ion-icon>
<!-- Ionic components -->
<ion-action-sheet></ion-action-sheet>
<ion-alert></ion-alert>
<ion-back-button></ion-back-button>
<ion-badge></ion-badge>
<ion-button></ion-button>
<ion-buttons></ion-buttons>
<ion-card></ion-card>
<ion-card-content></ion-card-content>
<ion-card-header></ion-card-header>
<ion-checkbox></ion-checkbox>
<ion-content></ion-content>
<ion-footer></ion-footer>
<ion-grid></ion-grid>
<ion-header></ion-header>
<ion-popover></ion-popover>
<ion-content>
<ion-refresher slot="fixed"></ion-refresher>
<ion-refresher-content pullingContent="lines"></ion-refresher-content>
<ion-infinite-scroll></ion-infinite-scroll>
<ion-infinite-scroll-content
loadingSpinner="lines"
></ion-infinite-scroll-content>
</ion-content>
<ion-input></ion-input>
<ion-item></ion-item>
<ion-item-divider></ion-item-divider>
<ion-item-group></ion-item-group>
<ion-label></ion-label>
<ion-list></ion-list>
<ion-loading></ion-loading>
<ion-modal></ion-modal>
<ion-note></ion-note>
<ion-radio></ion-radio>
<ion-reorder></ion-reorder>
<ion-row></ion-row>
<ion-searchbar></ion-searchbar>
<ion-segment></ion-segment>
<ion-segment-button></ion-segment-button>
<ion-select></ion-select>
<ion-select-option></ion-select-option>
<ion-slides></ion-slides>
<ion-spinner name="lines"></ion-spinner>
<ion-text></ion-text>
<ion-text style="font-weight: bold">load bold</ion-text>
<ion-title></ion-title>
<ion-toast></ion-toast>
<ion-toggle></ion-toggle>
<ion-toolbar></ion-toolbar>
<ion-menu-button></ion-menu-button>
<!-- fonts -->
<p style="font-family: Montserrat">a</p>
<p style="font-family: Montserrat; font-weight: bold">a</p>
<p style="font-family: Montserrat; font-weight: 100">a</p>
<p style="font-family: Open Sans">a</p>
<p style="font-family: Open Sans; font-weight: bold">a</p>
<p style="font-family: Open Sans; font-weight: 100">a</p>
<!-- images -->
<img src="assets/img/logo.png" />
<img src="assets/img/icons/snek.png" />
<img src="assets/img/icons/wifi-1.png" />
<img src="assets/img/icons/wifi-2.png" />
<img src="assets/img/icons/wifi-3.png" />
</section>
<section appPreloader></section>
</ion-content>
<ion-footer
[ngStyle]="{
'max-height': osUpdateProgress ? '100px' : '0px',
overflow: 'hidden',
'transition-property': 'max-height',
'transition-duration': '1s',
'transition-delay': '.05s'
}"
>
<ion-toolbar
style="border-top: 1px solid var(--ion-color-dark)"
color="light"
>
<ion-list>
<ion-list-header>
<ion-label
>Downloading EOS:
{{
(
(100 * (osUpdateProgress?.downloaded || 1)) /
(osUpdateProgress?.size || 1)
).toFixed(0)
}}%</ion-label
>
</ion-list-header>
<div style="padding: 0 16px 16px 16px">
<ion-progress-bar
color="secondary"
[value]="
osUpdateProgress &&
osUpdateProgress.downloaded / osUpdateProgress.size
"
></ion-progress-bar>
</div>
</ion-list>
</ion-toolbar>
<ion-footer>
<footer appFooter></footer>
</ion-footer>
</ion-app>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
<ion-toolbar
*ngIf="progress$ | async as progress"
color="light"
[@heightCollapse]="animation"
>
<ion-list class="list">
<ion-list-header>
<ion-label>Downloading EOS: {{ getProgress(progress) }}%</ion-label>
</ion-list-header>
<ion-progress-bar
class="progress"
color="secondary"
[value]="getProgress(progress) / 100"
></ion-progress-bar>
</ion-list>
</ion-toolbar>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,60 @@
<a class="logo ion-padding" target="_blank" rel="noreferrer" [href]="href">
<img alt="Start9" src="assets/img/logo.png" />
</a>
<div class="divider"></div>
<ion-item-group class="menu">
<ion-menu-toggle *ngFor="let page of pages; let i = index" auto-hide="false">
<ion-item
*ngIf="
page.url !== '/developer' || (localStorageService.showDevTools$ | async)
"
button
class="link"
color="transparent"
routerDirection="root"
lines="none"
detail="false"
[routerLink]="page.url"
>
<ion-icon
slot="start"
class="icon label"
routerLinkActive="label_selected"
[name]="page.icon"
></ion-icon>
<ion-label class="label montserrat" routerLinkActive="label_selected">
{{ page.title }}
</ion-label>
<ion-icon
*ngIf="page.url === '/embassy' && eosService.updateAvailable$ | async"
color="success"
size="small"
name="rocket-outline"
></ion-icon>
<ion-badge
*ngIf="page.url === '/notifications' && notification$ | async as count"
color="danger"
class="badge"
>
{{ count }}
</ion-badge>
</ion-item>
</ion-menu-toggle>
</ion-item-group>
<img appSnek class="snek" alt="Play Snek" src="assets/img/icons/snek.png" />
<div class="bottom">
<div class="divider" style="margin-bottom: 10px"></div>
<ion-menu-toggle auto-hide="false">
<ion-item
button
lines="none"
style="--background: transparent; margin-bottom: 86px; text-align: center"
fill="clear"
(click)="presentAlertLogout()"
>
<ion-label>
<ion-text class="montserrat" color="dark"> Log Out </ion-text>
</ion-label>
</ion-item>
</ion-menu-toggle>
</div>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,71 @@
<!-- Ionicons -->
<ion-icon *ngFor="let icon of icons" [name]="icon"></ion-icon>
<!-- 3rd party components -->
<qr-code value="hello"></qr-code>
<!-- Ionic components -->
<ion-action-sheet></ion-action-sheet>
<ion-alert></ion-alert>
<ion-back-button></ion-back-button>
<ion-badge></ion-badge>
<ion-button></ion-button>
<ion-buttons></ion-buttons>
<ion-card></ion-card>
<ion-card-content></ion-card-content>
<ion-card-header></ion-card-header>
<ion-checkbox></ion-checkbox>
<ion-content></ion-content>
<ion-footer></ion-footer>
<ion-grid></ion-grid>
<ion-header></ion-header>
<ion-popover></ion-popover>
<ion-content>
<ion-refresher slot="fixed"></ion-refresher>
<ion-refresher-content pullingContent="lines"></ion-refresher-content>
<ion-infinite-scroll></ion-infinite-scroll>
<ion-infinite-scroll-content
loadingSpinner="lines"
></ion-infinite-scroll-content>
</ion-content>
<ion-input></ion-input>
<ion-item></ion-item>
<ion-item-divider></ion-item-divider>
<ion-item-group></ion-item-group>
<ion-label></ion-label>
<ion-list></ion-list>
<ion-loading></ion-loading>
<ion-modal></ion-modal>
<ion-note></ion-note>
<ion-radio></ion-radio>
<ion-reorder></ion-reorder>
<ion-row></ion-row>
<ion-searchbar></ion-searchbar>
<ion-segment></ion-segment>
<ion-segment-button></ion-segment-button>
<ion-select></ion-select>
<ion-select-option></ion-select-option>
<ion-slides></ion-slides>
<ion-spinner name="lines"></ion-spinner>
<ion-text></ion-text>
<ion-text><strong>load bold font</strong></ion-text>
<ion-title></ion-title>
<ion-toast></ion-toast>
<ion-toggle></ion-toggle>
<ion-toolbar></ion-toolbar>
<ion-menu-button></ion-menu-button>
<!-- fonts -->
<p style="font-family: Montserrat">a</p>
<p style="font-family: Montserrat; font-weight: bold">a</p>
<p style="font-family: Montserrat; font-weight: 100">a</p>
<p style="font-family: Open Sans">a</p>
<p style="font-family: Open Sans; font-weight: bold">a</p>
<p style="font-family: Open Sans; font-weight: 100">a</p>
<!-- images -->
<img src="assets/img/logo.png" />
<img src="assets/img/icons/snek.png" />
<img src="assets/img/icons/wifi-1.png" />
<img src="assets/img/icons/wifi-2.png" />
<img src="assets/img/icons/wifi-3.png" />

View File

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

View File

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

View File

@@ -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<string, boolean>([
...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()
}
}

View File

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

View File

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

View File

@@ -5,7 +5,7 @@
<img [src]="dep.icon" alt="" />
</ion-thumbnail>
<ion-label>
<h2 class="inline">
<h2 class="montserrat">
<ion-icon
*ngIf="!!dep.errorText"
class="icon"

View File

@@ -1,7 +1,3 @@
.inline {
font-family: 'Montserrat', sans-serif;
}
.icon {
padding-right: 4px;
}

View File

@@ -8,7 +8,10 @@
<img [src]="pkg['static-files'].icon" alt="" />
</ion-avatar>
<ion-label>
<h1 class="name" [class.less-large]="pkg.manifest.title.length > 20">
<h1
class="montserrat"
[class.less-large]="pkg.manifest.title.length > 20"
>
{{ pkg.manifest.title }}
</h1>
<h2>{{ pkg.manifest.version | displayEmver }}</h2>

View File

@@ -1,7 +1,3 @@
.name {
font-family: 'Montserrat', sans-serif;
}
.less-large {
font-size: 18px !important;
}

View File

@@ -1,4 +1,4 @@
<h1 class="heading ion-text-center">{{ name }}</h1>
<h1 class="heading montserrat ion-text-center">{{ name }}</h1>
<marketplace-search [(query)]="query"></marketplace-search>

View File

@@ -1,5 +1,4 @@
.heading {
font-family: 'Montserrat', sans-serif;
font-size: 42px;
margin: 32px 0;
}

View File

@@ -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<Record<string, PackageDataEntry>> = 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<Record<string, PackageDataEntry>> = 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<MarketplacePkg[]> = defer(() =>
this.patch.watch$('server-info'),
).pipe(
filter(data => exists(data) && !isEmptyObject(data)),
first(),
switchMapTo(this.marketplaceService.getPackages()),
)
readonly pkgs$: Observable<MarketplacePkg[]> = this.patch
.watch$('server-info')
.pipe(
filter(data => exists(data) && !isEmptyObject(data)),
first(),
switchMapTo(this.marketplaceService.getPackages()),
)
readonly name$: Observable<string> = this.marketplaceService
.getMarketplace()

View File

@@ -2,7 +2,7 @@
<ion-item *ngIf="dependentInfo" lines="none" class="rec-item">
<ion-label>
<h2 class="heading">
<ion-text class="title">
<ion-text class="montserrat title">
{{ title }}
</ion-text>
</h2>

View File

@@ -5,7 +5,6 @@
.title {
margin: 5px;
font-family: 'Montserrat', sans-serif;
font-size: 18px;
}

View File

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

View File

@@ -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<Source<DataModel>>('')
export const PATCH_SOURCE = new InjectionToken<Source<DataModel>>('')
@@ -176,9 +180,19 @@ export class PatchDbService {
// prettier-ignore
watch$: Store<DataModel>['watch$'] = (...args: (string | number)[]): Observable<DataModel> => {
// 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)

View File

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