mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
refactor: decompose app component
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
36
frontend/projects/ui/src/app/app/footer/footer.component.ts
Normal file
36
frontend/projects/ui/src/app/app/footer/footer.component.ts
Normal 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))
|
||||
}
|
||||
}
|
||||
12
frontend/projects/ui/src/app/app/footer/footer.module.ts
Normal file
12
frontend/projects/ui/src/app/app/footer/footer.module.ts
Normal 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 {}
|
||||
60
frontend/projects/ui/src/app/app/menu/menu.component.html
Normal file
60
frontend/projects/ui/src/app/app/menu/menu.component.html
Normal 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>
|
||||
47
frontend/projects/ui/src/app/app/menu/menu.component.scss
Normal file
47
frontend/projects/ui/src/app/app/menu/menu.component.scss
Normal 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;
|
||||
}
|
||||
93
frontend/projects/ui/src/app/app/menu/menu.component.ts
Normal file
93
frontend/projects/ui/src/app/app/menu/menu.component.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
14
frontend/projects/ui/src/app/app/menu/menu.module.ts
Normal file
14
frontend/projects/ui/src/app/app/menu/menu.module.ts
Normal 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 {}
|
||||
@@ -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" />
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {}
|
||||
88
frontend/projects/ui/src/app/app/snek/snek.directive.ts
Normal file
88
frontend/projects/ui/src/app/app/snek/snek.directive.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
11
frontend/projects/ui/src/app/app/snek/snek.module.ts
Normal file
11
frontend/projects/ui/src/app/app/snek/snek.module.ts
Normal 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 {}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
.inline {
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
.name {
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
}
|
||||
|
||||
.less-large {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
.heading {
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-size: 42px;
|
||||
margin: 32px 0;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
.title {
|
||||
margin: 5px;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
17
frontend/projects/ui/src/app/util/animations.ts
Normal file
17
frontend/projects/ui/src/app/util/animations.ts
Normal 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,
|
||||
),
|
||||
])
|
||||
Reference in New Issue
Block a user