more efficient subscriptions, more styling

This commit is contained in:
Matt Hill
2021-07-02 00:05:05 -06:00
committed by Aiden McClelland
parent 330d5a08af
commit da3aa0a2a7
53 changed files with 583 additions and 585 deletions

View File

@@ -1,9 +1,9 @@
<ion-app>
<ion-split-pane *ngIf="patch.patchDb?.store" [disabled]="!showMenu" (ionSplitPaneVisible)="splitPaneVisible($event)" contentId="main-content">
<ion-split-pane [disabled]="!showMenu" (ionSplitPaneVisible)="splitPaneVisible($event)" contentId="main-content">
<ion-menu contentId="main-content" type="overlay">
<ion-header>
<ion-toolbar style="--background: var(--ion-background-color);">
<ion-title *ngIf="patch.watch$('ui', 'server-name') | ngrxPush as name; else dots">{{ name }}</ion-title>
<ion-title *ngIf="serverName; else dots">{{ serverName }}</ion-title>
<ng-template #dots>
<ion-title><ion-spinner name="dots" color="warning"></ion-spinner></ion-title>
</ng-template>
@@ -24,7 +24,7 @@
>
<ion-icon slot="start" [name]="page.icon"></ion-icon>
<ion-label style="font-family: 'Montserrat';">{{ page.title }}</ion-label>
<ion-badge *ngIf="page.url === '/notifications' && (patch.watch$('server-info', 'unread-notification-count') | ngrxPush) as badge" color="danger" style="margin-right: 3%;" [class.selected-badge]="selectedIndex == i">{{ badge }}</ion-badge>
<ion-badge *ngIf="page.url === '/notifications' && unreadCount" color="danger" style="margin-right: 3%;" [class.selected-badge]="selectedIndex == i">{{ unreadCount }}</ion-badge>
</ion-item>
</ion-menu-toggle>
</ion-list>

View File

@@ -13,6 +13,7 @@ import { PatchDbModel } from './models/patch-db/patch-db-model'
import { HttpService } from './services/http.service'
import { ServerStatus } from './models/patch-db/data-model'
import { ConnectionFailure, ConnectionService } from './services/connection.service'
import { combineLatest, merge } from 'rxjs'
@Component({
selector: 'app-root',
@@ -23,8 +24,9 @@ export class AppComponent {
ServerStatus = ServerStatus
showMenu = false
selectedIndex = 0
untilLoaded = true
offlineToast: HTMLIonToastElement
serverName: string
unreadCount: number
appPages = [
{
title: 'Services',
@@ -59,8 +61,8 @@ export class AppComponent {
private readonly emver: Emver,
private readonly connectionService: ConnectionService,
private readonly toastCtrl: ToastController,
private readonly patch: PatchDbModel,
readonly splitPane: SplitPaneTracker,
readonly patch: PatchDbModel,
) {
// set dark theme
document.body.classList.toggle('dark', true)
@@ -83,8 +85,10 @@ export class AppComponent {
this.http.authReqEnabled = true
this.showMenu = true
this.patch.start()
// watch patch DB to display name and unread count
this.watchPatch()
this.connectionService.start()
// watch network
// watch connection to display connectivity issues
this.watchConnection(auth)
// watch router to highlight selected menu item
this.watchRouter(auth)
@@ -108,6 +112,17 @@ export class AppComponent {
})
}
watchPatch (): void {
combineLatest([
this.patch.watch$('ui', 'server-name'),
this.patch.watch$('server-info', 'unread-notification-count'),
])
.subscribe(([name, unread]) => {
this.serverName = name
this.unreadCount = unread
})
}
private watchConnection (auth: AuthState): void {
this.connectionService.watch$()
.pipe(
@@ -265,7 +280,7 @@ export class AppComponent {
}
splitPaneVisible (e: any) {
this.splitPane.menuFixedOpenOnLeft$.next(e.detail.visible)
this.splitPane.sidebarOpen$.next(e.detail.visible)
}
}

View File

@@ -1,4 +1,4 @@
<div style="position: relative; margin-right: 1vh;">
<ion-badge mode="md" class="md-badge" *ngIf="(badge$ | ngrxPush) && !(menuFixedOpen$ | ngrxPush)" color="danger">{{ badge$ | ngrxPush }}</ion-badge>
<ion-badge mode="md" class="md-badge" *ngIf="unreadCount && !sidebarOpen" color="danger">{{ unreadCount }}</ion-badge>
<ion-menu-button color="dark"></ion-menu-button>
</div>

View File

@@ -1,7 +1,7 @@
import { Component } from '@angular/core'
import { Observable } from 'rxjs'
import { SplitPaneTracker } from 'src/app/services/split-pane.service'
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
import { combineLatest, Subscription } from 'rxjs'
@Component({
selector: 'badge-menu-button',
@@ -10,14 +10,30 @@ import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
})
export class BadgeMenuComponent {
badge$: Observable<number>
menuFixedOpen$: Observable<boolean>
unreadCount: number
sidebarOpen: boolean
subs: Subscription[] = []
constructor (
private readonly splitPane: SplitPaneTracker,
private readonly patch: PatchDbModel,
) {
this.menuFixedOpen$ = this.splitPane.menuFixedOpenOnLeft$.asObservable()
this.badge$ = this.patch.watch$('server-info', 'unread-notification-count')
) { }
ngOnInit () {
this.subs = [
combineLatest([
this.patch.watch$('server-info', 'unread-notification-count'),
this.splitPane.sidebarOpen$,
])
.subscribe(([unread, menu]) => {
this.unreadCount = unread
this.sidebarOpen = menu
}),
]
}
ngOnDestroy () {
this.subs.forEach(sub => sub.unsubscribe())
}
}

View File

@@ -8,7 +8,7 @@
</ion-header>
<ion-content>
<ion-slides *ngIf="!(error$ | ngrxPush)" id="slide-show" style="--bullet-background: white" pager="false">
<ion-slides *ngIf="!error" id="slide-show" style="--bullet-background: white" pager="false">
<ion-slide *ngFor="let def of params.slideDefinitions">
<!-- We can pass [transitions]="transitions" into the component if logic within the component needs to trigger a transition (not just bottom bar) -->
<dependencies #components *ngIf="def.slide.selector === 'dependencies'" [params]="def.slide.params"></dependencies>
@@ -19,7 +19,7 @@
</ion-slides>
<div *ngIf="error$ | ngrxPush as error" class="slide-content">
<div *ngIf="error" class="slide-content">
<div style="margin-top: 25px;">
<div style="margin: 15px; display: flex; justify-content: center; align-items: center;">
<ion-label color="danger" style="font-size: xx-large; font-weight: bold;">
@@ -35,7 +35,7 @@
<ion-footer>
<ion-toolbar style="padding: 8px;">
<ng-container *ngIf="!(initializing$ | ngrxPush) && !(error$ | ngrxPush) && { loading: currentSlideloading$ | ngrxPush, bar: currentBottomBar} as v">
<ng-container *ngIf="!initializing && !error">
<!-- cancel button if loading/not loading -->
<ion-button slot="start" *ngIf="v.loading && v.bar.cancel.whileLoading as cancel" (click)="transitions.cancel()" class="toolbar-button" fill="outline" color="medium">
@@ -58,7 +58,7 @@
</ion-button>
</ng-container>
<ng-container *ngIf="error$ | ngrxPush">
<ng-container *ngIf="error">
<ion-button slot="start" (click)="transitions.final()" style="text-transform: capitalize; font-weight: bolder;" color="danger">Dismiss</ion-button>
</ng-container>
</ion-toolbar>

View File

@@ -1,6 +1,5 @@
import { Component, Input, NgZone, QueryList, ViewChild, ViewChildren } from '@angular/core'
import { IonContent, IonSlides, ModalController } from '@ionic/angular'
import { BehaviorSubject } from 'rxjs'
import { capitalizeFirstLetter, pauseFor } from 'src/app/util/misc.util'
import { CompleteComponent } from './complete/complete.component'
import { DependentsComponent } from './dependents/dependents.component'
@@ -39,8 +38,8 @@ export class InstallWizardComponent {
return this.params.slideDefinitions[this.slideIndex].bottomBar
}
initializing$ = new BehaviorSubject(true)
error$ = new BehaviorSubject(undefined)
initializing = true
error = ''
constructor (
private readonly modalController: ModalController,
@@ -54,7 +53,7 @@ export class InstallWizardComponent {
}
ionViewDidEnter () {
this.initializing$.next(false)
this.initializing = false
}
// process bottom bar buttons
@@ -62,7 +61,7 @@ export class InstallWizardComponent {
const i = info as { next?: any, error?: Error, cancelled?: true, final?: true }
if (i.cancelled) this.currentSlide.cancel$.next()
if (i.final || i.cancelled) return this.modalController.dismiss(i)
if (i.error) return this.error$.next(capitalizeFirstLetter(i.error.message))
if (i.error) return this.error = capitalizeFirstLetter(i.error.message)
this.moveToNextSlide(i.next)
}

View File

@@ -1,153 +0,0 @@
// import { MapSubject, Delta, Update } from '../util/map-subject.util'
// import { diff, partitionArray } from '../util/misc.util'
// import { Injectable } from '@angular/core'
// import { merge, Observable, of } from 'rxjs'
// import { filter, throttleTime, delay, pairwise, mapTo, take } from 'rxjs/operators'
// import { Storage } from '@ionic/storage'
// import { StorageKeys } from './storage-keys'
// import { AppInstalledFull, AppInstalledPreview } from './app-types'
// @Injectable({
// providedIn: 'root',
// })
// export class AppModel extends MapSubject<AppInstalledFull> {
// // hasLoaded tells us if we've successfully queried apps from api or storage, even if there are none.
// hasLoaded = false
// lastUpdatedAt: { [id: string]: Date } = { }
// constructor (private readonly storage: Storage) {
// super()
// // 500ms after first delta, will save to db. Subsequent deltas are ignored for those 500ms.
// // Process continues as long as deltas fire.
// this.watchDelta().pipe(throttleTime(200), delay(200)).subscribe(() => {
// this.commitCache()
// })
// }
// update (newValues: Update<AppInstalledFull>, timestamp: Date = new Date()): void {
// this.lastUpdatedAt[newValues.id] = this.lastUpdatedAt[newValues.id] || timestamp
// if (this.lastUpdatedAt[newValues.id] > timestamp) {
// return
// } else {
// super.update(newValues)
// this.lastUpdatedAt[newValues.id] = timestamp
// }
// }
// // client fxns
// watchDelta (filterFor?: Delta<AppInstalledFull>['action']): Observable<Delta<AppInstalledFull>> {
// return filterFor
// ? this.$delta$.pipe(filter(d => d.action === filterFor))
// : this.$delta$.asObservable()
// }
// watch (appId: string) : PropertySubject<AppInstalledFull> {
// const toReturn = super.watch(appId)
// if (!toReturn) throw new Error(`Expected Service ${appId} but not found.`)
// return toReturn
// }
// // when an app is installing
// watchForInstallation (appId: string): Observable<string | undefined> {
// const toWatch = super.watch(appId)
// if (!toWatch) return of(undefined)
// return toWatch.status.pipe(
// filter(s => s !== AppStatus.UNREACHABLE && s !== AppStatus.UNKNOWN),
// pairwise(),
// filter( ([old, _]) => old === AppStatus.INSTALLING ),
// take(1),
// mapTo(appId),
// )
// }
// // TODO: EJECT-DISKS: we can use this to watch for an app completing its backup process.
// watchForBackup (appId: string): Observable<string | undefined> {
// const toWatch = super.watch(appId)
// if (!toWatch) return of(undefined)
// return toWatch.status.pipe(
// filter(s => s !== AppStatus.UNREACHABLE && s !== AppStatus.UNKNOWN),
// pairwise(),
// filter( ([old, _]) => old === AppStatus.CREATING_BACKUP),
// take(1),
// mapTo(appId),
// )
// }
// watchForInstallations (appIds: { id: string }[]): Observable<string> {
// return merge(...appIds.map(({ id }) => this.watchForInstallation(id))).pipe(
// filter(t => !!t),
// )
// }
// // cache mgmt
// clear (): void {
// this.ids.forEach(id => {
// complete(this.contents[id] || { } as PropertySubject<any>)
// delete this.contents[id]
// })
// this.hasLoaded = false
// this.contents = { }
// this.lastUpdatedAt = { }
// }
// private commitCache (): Promise<void> {
// return this.storage.set(StorageKeys.APPS_CACHE_KEY, this.all || [])
// }
// async restoreCache (): Promise<void> {
// const stored = await this.storage.get(StorageKeys.APPS_CACHE_KEY)
// console.log(`restored app cache`, stored)
// if (stored) this.hasLoaded = true
// return (stored || []).map(c => this.add({ ...emptyAppInstalledFull(), ...c, status: AppStatus.UNKNOWN }))
// }
// upsertAppFull (app: AppInstalledFull): void {
// this.update(app)
// }
// // synchronizers
// upsertApps (apps: AppInstalledPreview[], timestamp: Date): void {
// const [updates, creates] = partitionArray(apps, a => !!this.contents[a.id])
// updates.map(u => this.update(u, timestamp))
// creates.map(c => this.add({ ...emptyAppInstalledFull(), ...c }))
// }
// syncCache (upToDateApps : AppInstalledPreview[], timestamp: Date) {
// this.hasLoaded = true
// this.deleteNonexistentApps(upToDateApps)
// this.upsertApps(upToDateApps, timestamp)
// }
// private deleteNonexistentApps (apps: AppInstalledPreview[]): void {
// const currentAppIds = apps.map(a => a.id)
// const previousAppIds = Object.keys(this.contents)
// const appsToDelete = diff(previousAppIds, currentAppIds)
// appsToDelete.map(appId => this.delete(appId))
// }
// // server state change
// markAppsUnreachable (): void {
// this.updateAllApps({ status: AppStatus.UNREACHABLE })
// }
// markAppsUnknown (): void {
// this.updateAllApps({ status: AppStatus.UNKNOWN })
// }
// private updateAllApps (uniformUpdate: Partial<AppInstalledFull>) {
// this.ids.map(id => {
// this.update(Object.assign(uniformUpdate, { id }))
// })
// }
// }
// function emptyAppInstalledFull (): Omit<AppInstalledFull, keyof AppInstalledPreview> {
// return {
// instructions: null,
// lastBackup: null,
// configuredRequirements: null,
// hasFetchedFull: false,
// actions: [],
// }
// }

View File

@@ -3,11 +3,7 @@ import { ConfigSpec } from 'src/app/pkg-config/config-types'
export interface DataModel {
'server-info': ServerInfo
'package-data': { [id: string]: PackageDataEntry }
ui: {
'server-name': string
'welcome-ack': string
'auto-check-updates': boolean
}
ui: UIData
}
export interface ServerInfo {
@@ -381,3 +377,9 @@ export interface InterfaceInfo {
}
export type URL = string
export interface UIData {
'server-name': string
'welcome-ack': string
'auto-check-updates': boolean
}

View File

@@ -3,7 +3,6 @@ import { EmverComparesPipe, EmverSatisfiesPipe, EmverDisplayPipe } from '../pipe
import { IncludesPipe } from '../pipes/includes.pipe'
import { TypeofPipe } from '../pipes/typeof.pipe'
import { MarkdownPipe } from '../pipes/markdown.pipe'
// import { InstalledLatestComparisonPipe, InstalledViewingComparisonPipe } from '../pipes/installed-latest-comparison.pipe'
import { AnnotationStatusPipe } from '../pipes/annotation-status.pipe'
import { TruncateCenterPipe, TruncateEndPipe } from '../pipes/truncate.pipe'
import { MaskPipe } from '../pipes/mask.pipe'
@@ -21,8 +20,6 @@ import { ReactiveComponentModule } from '@ngrx/component'
TypeofPipe,
IncludesPipe,
MarkdownPipe,
// InstalledLatestComparisonPipe,
// InstalledViewingComparisonPipe,
AnnotationStatusPipe,
TruncateCenterPipe,
TruncateEndPipe,
@@ -45,8 +42,6 @@ import { ReactiveComponentModule } from '@ngrx/component'
TypeofPipe,
IncludesPipe,
MarkdownPipe,
// InstalledLatestComparisonPipe,
// InstalledViewingComparisonPipe,
AnnotationStatusPipe,
TruncateEndPipe,
TruncateCenterPipe,

View File

@@ -8,7 +8,7 @@
</ion-header>
<ion-content class="ion-padding-top">
<ng-container *ngIf="patch.watch$('package-data', pkgId, 'installed') | ngrxPush as installed">
<ng-container *ngIf="installed">
<ion-item-group>
<ion-item button *ngFor="let action of installed.manifest.actions | keyvalue: asIsOrder" (click)="handleAction(installed, action)" >
<ion-label class="ion-text-wrap">

View File

@@ -1,13 +1,14 @@
import { Component } from '@angular/core'
import { Component, ViewChild } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { ApiService } from 'src/app/services/api/api.service'
import { AlertController, ModalController, NavController } from '@ionic/angular'
import { AlertController, IonContent, ModalController, NavController } from '@ionic/angular'
import { LoaderService } from 'src/app/services/loader.service'
import { HttpErrorResponse } from '@angular/common/http'
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
import { Action, InstalledPackageDataEntry, Manifest, PackageMainStatus } from 'src/app/models/patch-db/data-model'
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
import { Subscription } from 'rxjs'
@Component({
selector: 'app-actions',
@@ -15,7 +16,10 @@ import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
styleUrls: ['./app-actions.page.scss'],
})
export class AppActionsPage {
pkgId: string
installed: InstalledPackageDataEntry
subs: Subscription[] = []
@ViewChild(IonContent) content: IonContent
constructor (
private readonly route: ActivatedRoute,
@@ -25,11 +29,26 @@ export class AppActionsPage {
private readonly loaderService: LoaderService,
private readonly wizardBaker: WizardBaker,
private readonly navCtrl: NavController,
public readonly patch: PatchDbModel,
private readonly patch: PatchDbModel,
) { }
ngOnInit () {
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
const pkgId = this.route.snapshot.paramMap.get('pkgId')
this.subs = [
this.patch.watch$('package-data', pkgId, 'installed')
.subscribe(installed => {
this.installed = installed
}),
]
}
ngAfterViewInit () {
this.content.scrollToPoint(undefined, 1)
}
ngOnDestroy () {
this.subs.forEach(sub => sub.unsubscribe())
}
async handleAction (pkg: InstalledPackageDataEntry, action: { key: string, value: Action }) {

View File

@@ -12,7 +12,7 @@
<ion-content class="ion-padding-top">
<!-- loading -->
<ion-grid *ngIf="loadingText$ | ngrxPush as loadingText; else loaded" style="height: 100%;">
<ion-grid *ngIf="loadingText; else loaded" style="height: 100%;">
<ion-row class="ion-align-items-center ion-text-center" style="height: 100%;">
<ion-col>
<ion-spinner name="lines" color="warning"></ion-spinner>

View File

@@ -1,11 +1,11 @@
import { Component } from '@angular/core'
import { NavController, AlertController, ModalController, PopoverController } from '@ionic/angular'
import { Component, ViewChild } from '@angular/core'
import { NavController, AlertController, ModalController, PopoverController, IonContent } from '@ionic/angular'
import { ActivatedRoute } from '@angular/router'
import { ApiService } from 'src/app/services/api/api.service'
import { isEmptyObject } from 'src/app/util/misc.util'
import { LoaderService } from 'src/app/services/loader.service'
import { TrackingModalController } from 'src/app/services/tracking-modal-controller.service'
import { BehaviorSubject, from, fromEvent, of, Subscription } from 'rxjs'
import { from, fromEvent, of, Subscription } from 'rxjs'
import { catchError, concatMap, map, take, tap } from 'rxjs/operators'
import { Recommendation } from 'src/app/components/recommendation-button/recommendation-button.component'
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
@@ -26,7 +26,7 @@ export class AppConfigPage {
{ title: string, description: string, buttonText: string }
}
loadingText$ = new BehaviorSubject(undefined)
loadingText: string | undefined
pkg: InstalledPackageDataEntry
hasConfig = false
@@ -45,7 +45,8 @@ export class AppConfigPage {
spec: ConfigSpec
config: object
subs: Subscription[]
@ViewChild(IonContent) content: IonContent
subs: Subscription[] = []
constructor (
private readonly navCtrl: NavController,
@@ -84,47 +85,50 @@ export class AppConfigPage {
this.navCtrl.back()
}
}),
]
this.patch.watch$('package-data', pkgId, 'installed')
.pipe(
tap(pkg => this.pkg = pkg),
tap(() => this.loadingText$.next(`Fetching config spec...`)),
concatMap(() => this.apiService.getPackageConfig({ id: pkgId })),
concatMap(({ spec, config }) => {
const rec = history.state && history.state.configRecommendation as Recommendation
if (rec) {
this.loadingText$.next(`Setting properties to accommodate ${rec.dependentTitle}...`)
return from(this.apiService.dryConfigureDependency({ 'dependency-id': pkgId, 'dependent-id': rec.dependentId }))
.pipe(
map(res => ({
spec,
config,
dependencyConfig: res,
})),
tap(() => this.rec = rec),
catchError(e => {
this.error = { text: `Could not set properties to accommodate ${rec.dependentTitle}: ${e.message}`, moreInfo: {
title: `${rec.dependentTitle} requires the following:`,
description: rec.description,
buttonText: 'Configure Manually',
} }
return of({ spec, config, dependencyConfig: null })
}),
)
} else {
return of({ spec, config, dependencyConfig: null })
}
this.patch.watch$('package-data', pkgId, 'installed')
.pipe(
tap(pkg => this.pkg = pkg),
tap(() => this.loadingText = 'Fetching config spec...'),
concatMap(() => this.apiService.getPackageConfig({ id: pkgId })),
concatMap(({ spec, config }) => {
const rec = history.state && history.state.configRecommendation as Recommendation
if (rec) {
this.loadingText = `Setting properties to accommodate ${rec.dependentTitle}...`
return from(this.apiService.dryConfigureDependency({ 'dependency-id': pkgId, 'dependent-id': rec.dependentId }))
.pipe(
map(res => ({
spec,
config,
dependencyConfig: res,
})),
tap(() => this.rec = rec),
catchError(e => {
this.error = { text: `Could not set properties to accommodate ${rec.dependentTitle}: ${e.message}`, moreInfo: {
title: `${rec.dependentTitle} requires the following:`,
description: rec.description,
buttonText: 'Configure Manually',
} }
return of({ spec, config, dependencyConfig: null })
}),
)
} else {
return of({ spec, config, dependencyConfig: null })
}
}),
map(({ spec, config, dependencyConfig }) => this.setConfig(spec, config, dependencyConfig)),
tap(() => this.loadingText = undefined),
take(1),
).subscribe({
error: e => {
console.error(e.message)
this.error = { text: e.message }
},
}),
map(({ spec, config, dependencyConfig }) => this.setConfig(spec, config, dependencyConfig)),
tap(() => this.loadingText$.next(undefined)),
take(1),
).subscribe({
error: e => {
console.error(e.message)
this.error = { text: e.message }
},
})
]
}
ngAfterViewInit () {
this.content.scrollToPoint(undefined, 1)
}
ngOnDestroy () {

View File

@@ -1,9 +1,10 @@
import { Component } from '@angular/core'
import { Component, ViewChild } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { IonContent } from '@ionic/angular'
import { Subscription } from 'rxjs'
import { concatMap, take, tap } from 'rxjs/operators'
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
import { ApiService } from 'src/app/services/api/api.service'
import { Method } from 'src/app/services/http.service'
@Component({
selector: 'app-instructions',
@@ -15,6 +16,9 @@ export class AppInstructionsPage {
loading = true
error = ''
@ViewChild(IonContent) content: IonContent
subs: Subscription[] = []
constructor (
private readonly route: ActivatedRoute,
private readonly apiService: ApiService,
@@ -27,7 +31,6 @@ export class AppInstructionsPage {
.pipe(
concatMap(pkg => this.apiService.getStatic(pkg['static-files'].instructions)),
tap(instructions => {
console.log(instructions)
this.instructions = instructions
}),
take(1),
@@ -41,4 +44,12 @@ export class AppInstructionsPage {
() => console.log('COMPLETE'),
)
}
ngAfterViewInit () {
this.content.scrollToPoint(undefined, 1)
}
ngOnDestroy () {
this.subs.forEach(sub => sub.unsubscribe())
}
}

View File

@@ -8,7 +8,7 @@
</ion-header>
<ion-content class="ion-padding-top">
<ng-container *ngIf="patch.watch$('package-data', pkgId) | ngrxPush as pkg">
<ng-container *ngIf="pkg">
<ion-card style="margin-bottom: 16px;" *ngFor="let interface of pkg.installed.manifest.interfaces | keyvalue: asIsOrder">
<ion-card-header>

View File

@@ -1,7 +1,8 @@
import { Component, ViewChild } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { IonContent, ToastController } from '@ionic/angular'
import { InstalledPackageDataEntry } from 'src/app/models/patch-db/data-model'
import { Subscription } from 'rxjs'
import { InstalledPackageDataEntry, PackageDataEntry } from 'src/app/models/patch-db/data-model'
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
import { ConfigService } from 'src/app/services/config.service'
import { copyToClipboard } from 'src/app/util/web.util'
@@ -12,25 +13,33 @@ import { copyToClipboard } from 'src/app/util/web.util'
styleUrls: ['./app-Interfaces.page.scss'],
})
export class AppInterfacesPage {
pkgId: string
pkg: PackageDataEntry
@ViewChild(IonContent) content: IonContent
subs: Subscription[] = []
constructor (
private readonly route: ActivatedRoute,
private readonly toastCtrl: ToastController,
private readonly config: ConfigService,
public readonly patch: PatchDbModel,
private readonly patch: PatchDbModel,
) { }
ngOnInit () {
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
const pkgId = this.route.snapshot.paramMap.get('pkgId')
this.subs = [
this.patch.watch$('package-data', pkgId).subscribe(pkg => this.pkg = pkg),
]
}
async ngAfterViewInit () {
ngAfterViewInit () {
this.content.scrollToPoint(undefined, 1)
}
ngOnDestroy () {
this.subs.forEach(sub => sub.unsubscribe())
}
async copy (address: string): Promise<void> {
let message = ''
await copyToClipboard(address || '')

View File

@@ -39,7 +39,7 @@
<img class="bulb-off" *ngIf="pkg.value | displayBulb: 'off' : connected" src="assets/img/off-bulb.png"/>
<ion-card-header>
<status *ngIf="connected" [pkg]="pkg.value" size="calc(4px + .7vw)" weight="bold"></status>
<status *ngIf="connected" [pkg]="pkg.value" size="calc(8px + .4vw)" weight="bold"></status>
<ion-card-title>{{ (pkg.value | manifest).title }}</ion-card-title>
</ion-card-header>
</ion-card>

View File

@@ -12,7 +12,7 @@
}
ion-card-title {
font-size: calc(8px + .7vw);
font-size: calc(10px + .4vw);
color: white;
margin: 10px 0;
white-space: nowrap;

View File

@@ -5,8 +5,8 @@
</ion-buttons>
<ion-title>Logs</ion-title>
<ion-buttons slot="end" class="ion-padding-end">
<ion-button (click)="getLogs()" color="primary">
<ion-icon name="refresh-outline"></ion-icon>
<ion-button (click)="getLogs()">
<ion-icon slot="icon-only" name="refresh-outline"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>

View File

@@ -1,10 +1,11 @@
import { Component } from '@angular/core'
import { Component, ViewChild } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { Subscription } from 'rxjs'
import { PackageDataEntry } from 'src/app/models/patch-db/data-model'
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
import { getManifest } from 'src/app/services/config.service'
import * as JsonPointer from 'json-pointer'
import { IonContent } from '@ionic/angular'
@Component({
selector: 'app-manifest',
@@ -12,23 +13,24 @@ import * as JsonPointer from 'json-pointer'
styleUrls: ['./app-manifest.page.scss'],
})
export class AppManifestPage {
pkgId: string
pkg: PackageDataEntry
pointer: string
node: object
subs: Subscription[]
segmentValue: 'formatted' | 'raw' = 'formatted'
@ViewChild(IonContent) content: IonContent
subs: Subscription[] = []
constructor (
private readonly route: ActivatedRoute,
private readonly patch: PatchDbModel,
) { }
ngOnInit () {
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
const pkgId = this.route.snapshot.paramMap.get('pkgId')
this.subs = [
this.patch.watch$('package-data', this.pkgId)
this.patch.watch$('package-data', pkgId)
.subscribe(pkg => {
this.pkg = pkg
this.setNode()
@@ -38,6 +40,10 @@ export class AppManifestPage {
this.setNode()
}
ngAfterViewInit () {
this.content.scrollToPoint(undefined, 1)
}
ngOnDestroy () {
this.subs.forEach(sub => sub.unsubscribe())
}

View File

@@ -1,5 +1,6 @@
import { Component } from '@angular/core'
import { Component, ViewChild } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { IonContent } from '@ionic/angular'
import { Subscription } from 'rxjs'
import { PackageDataEntry } from 'src/app/models/patch-db/data-model'
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
@@ -10,9 +11,10 @@ import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
styleUrls: ['./app-metrics.page.scss'],
})
export class AppMetricsPage {
pkgId: string
pkg: PackageDataEntry
subs: Subscription[]
@ViewChild(IonContent) content: IonContent
subs: Subscription[] = []
constructor (
private readonly route: ActivatedRoute,
@@ -20,16 +22,20 @@ export class AppMetricsPage {
) { }
ngOnInit () {
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
const pkgId = this.route.snapshot.paramMap.get('pkgId')
this.subs = [
this.patch.watch$('package-data', this.pkgId)
this.patch.watch$('package-data', pkgId)
.subscribe(pkg => {
this.pkg = pkg
}),
]
}
ngAfterViewInit () {
this.content.scrollToPoint(undefined, 1)
}
ngOnDestroy () {
this.subs.forEach(sub => sub.unsubscribe())
}

View File

@@ -4,6 +4,11 @@
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>Values</ion-title>
<ion-buttons slot="end" class="ion-padding-end">
<ion-button (click)="refresh()">
<ion-icon slot="icon-only" name="refresh-outline"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
@@ -11,64 +16,58 @@
<ion-spinner *ngIf="loading; else loaded" class="center" name="lines" color="warning"></ion-spinner>
<ng-template #loaded>
<ng-container *ngIf="patch.watch$('package-data', pkgId) | ngrxPush as pkg">
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
</ion-refresher>
<ion-item *ngIf="error" style="margin-bottom: 16px;">
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
</ion-item>
<!-- not running -->
<ion-item *ngIf="pkg.installed.status.main.status !== FeStatus.Running" class="ion-margin-bottom">
<ion-label class="ion-text-wrap">
<p><ion-text color="warning">Service not running. Information on this page could be inaccurate.</ion-text></p>
</ion-label>
</ion-item>
<ion-item *ngIf="error" style="margin-bottom: 16px;">
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
</ion-item>
<!-- no properties -->
<ion-item *ngIf="properties | empty">
<ion-label class="ion-text-wrap">
<p>No values.</p>
</ion-label>
</ion-item>
<!-- properties -->
<ion-item-group *ngIf="!(properties | empty)">
<div *ngFor="let prop of node | keyvalue: asIsOrder">
<!-- object -->
<ion-item button detail="true" *ngIf="prop.value.type === 'object'" (click)="goToNested(prop.key)">
<ion-button *ngIf="prop.value.description" class="help-button" fill="clear" slot="start" (click)="presentDescription(prop, $event)">
<ion-icon size="small" slot="icon-only" name="help-circle-outline"></ion-icon>
<!-- not running -->
<ion-item *ngIf="notRunning" class="ion-margin-bottom">
<ion-label class="ion-text-wrap">
<p><ion-text color="warning">Service not running. Information on this page could be inaccurate.</ion-text></p>
</ion-label>
</ion-item>
<!-- no properties -->
<ion-item *ngIf="properties | empty">
<ion-label class="ion-text-wrap">
<p>No values.</p>
</ion-label>
</ion-item>
<!-- properties -->
<ion-item-group *ngIf="!(properties | empty)">
<div *ngFor="let prop of node | keyvalue: asIsOrder">
<!-- object -->
<ion-item button detail="true" *ngIf="prop.value.type === 'object'" (click)="goToNested(prop.key)">
<ion-button *ngIf="prop.value.description" class="help-button" fill="clear" slot="start" (click)="presentDescription(prop, $event)">
<ion-icon size="small" slot="icon-only" name="help-circle-outline"></ion-icon>
</ion-button>
<ion-label class="ion-text-wrap">
<h2>{{ prop.key }}</h2>
</ion-label>
</ion-item>
<!-- not object -->
<ion-item *ngIf="prop.value.type === 'string'">
<ion-button *ngIf="prop.value.description" class="help-button" fill="clear" slot="start" (click)="presentDescription(prop, $event)">
<ion-icon size="small" slot="icon-only" name="help-circle-outline"></ion-icon>
</ion-button>
<ion-label class="ion-text-wrap">
<h2>{{ prop.key }}</h2>
<p>{{ prop.value.masked && !unmasked[prop.key] ? (prop.value.value | mask ) : (prop.value.value | truncateEnd : 100) }}</p>
</ion-label>
<div slot="end" *ngIf="prop.value.copyable || prop.value.qr">
<ion-button *ngIf="prop.value.masked" fill="clear" (click)="toggleMask(prop.key)">
<ion-icon slot="icon-only" [name]="unmasked[prop.key] ? 'eye-off-outline' : 'eye-outline'" [color]="unmasked[prop.key] ? 'danger' : 'primary'" size="small"></ion-icon>
</ion-button>
<ion-label class="ion-text-wrap">
<h2>{{ prop.key }}</h2>
</ion-label>
</ion-item>
<!-- not object -->
<ion-item *ngIf="prop.value.type === 'string'">
<ion-button *ngIf="prop.value.description" class="help-button" fill="clear" slot="start" (click)="presentDescription(prop, $event)">
<ion-icon size="small" slot="icon-only" name="help-circle-outline"></ion-icon>
<ion-button *ngIf="prop.value.qr" fill="clear" (click)="showQR(prop.value.value)">
<ion-icon slot="icon-only" name="qr-code-outline" size="small" color="primary"></ion-icon>
</ion-button>
<ion-label class="ion-text-wrap">
<h2>{{ prop.key }}</h2>
<p>{{ prop.value.masked && !unmasked[prop.key] ? (prop.value.value | mask ) : (prop.value.value | truncateEnd : 100) }}</p>
</ion-label>
<div slot="end" *ngIf="prop.value.copyable || prop.value.qr">
<ion-button *ngIf="prop.value.masked" fill="clear" (click)="toggleMask(prop.key)">
<ion-icon slot="icon-only" [name]="unmasked[prop.key] ? 'eye-off-outline' : 'eye-outline'" [color]="unmasked[prop.key] ? 'danger' : 'primary'" size="small"></ion-icon>
</ion-button>
<ion-button *ngIf="prop.value.qr" fill="clear" (click)="showQR(prop.value.value)">
<ion-icon slot="icon-only" name="qr-code-outline" size="small" color="primary"></ion-icon>
</ion-button>
<ion-button *ngIf="prop.value.copyable" fill="clear" (click)="copy(prop.value.value)">
<ion-icon slot="icon-only" name="copy-outline" size="small" color="primary"></ion-icon>
</ion-button>
</div>
</ion-item>
</div>
</ion-item-group>
</ng-container>
<ion-button *ngIf="prop.value.copyable" fill="clear" (click)="copy(prop.value.value)">
<ion-icon slot="icon-only" name="copy-outline" size="small" color="primary"></ion-icon>
</ion-button>
</div>
</ion-item>
</div>
</ion-item-group>
</ng-template>
</ion-content>

View File

@@ -1,14 +1,15 @@
import { Component } from '@angular/core'
import { Component, ViewChild } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { ApiService } from 'src/app/services/api/api.service'
import { Subscription } from 'rxjs'
import { copyToClipboard } from 'src/app/util/web.util'
import { AlertController, NavController, PopoverController, ToastController } from '@ionic/angular'
import { AlertController, IonContent, NavController, PopoverController, ToastController } from '@ionic/angular'
import { PackageProperties } from 'src/app/util/properties.util'
import { QRComponent } from 'src/app/components/qr/qr.component'
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
import * as JsonPointer from 'json-pointer'
import { FEStatus } from 'src/app/services/pkg-status-rendering.service'
import { PackageMainStatus } from 'src/app/models/patch-db/data-model'
@Component({
selector: 'app-properties',
@@ -24,8 +25,10 @@ export class AppPropertiesPage {
properties: PackageProperties
node: PackageProperties
unmasked: { [key: string]: boolean } = { }
FeStatus = FEStatus
subs: Subscription[]
running = true
@ViewChild(IonContent) content: IonContent
subs: Subscription[] = []
constructor (
private readonly route: ActivatedRoute,
@@ -48,18 +51,23 @@ export class AppPropertiesPage {
this.pointer = queryParams['pointer']
this.node = JsonPointer.get(this.properties, this.pointer || '')
}),
this.patch.watch$('package-data', this.pkgId, 'installed', 'status', 'main', 'status')
.subscribe(status => {
this.running = status === PackageMainStatus.Running
}),
]
}
this.loading = false
ngAfterViewInit () {
this.content.scrollToPoint(undefined, 1)
}
ngOnDestroy () {
this.subs.forEach(sub => sub.unsubscribe())
}
async doRefresh (event: any) {
await this.getProperties(),
event.target.complete()
async refresh () {
await this.getProperties()
}
async presentDescription (property: { key: string, value: PackageProperties[''] }, e: Event) {
@@ -114,12 +122,15 @@ export class AppPropertiesPage {
}
private async getProperties (): Promise<void> {
this.loading = true
try {
this.properties = await this.apiService.getPackageProperties({ id: this.pkgId })
this.node = JsonPointer.get(this.properties, this.pointer || '')
} catch (e) {
console.error(e)
this.error = e.message
} finally {
this.loading = false
}
}
}

View File

@@ -4,15 +4,15 @@
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>Restore Backup</ion-title>
<ion-buttons slot="end">
<ion-button (click)="doRefresh()" color="primary">
<ion-icon slot="icon-only" name="reload-outline"></ion-icon>
<ion-buttons slot="end" class="ion-padding-end">
<ion-button (click)="refresh()">
<ion-icon slot="icon-only" name="refresh-outline"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding-top" *ngIf="patch.watch$('package-data', pkgId) | ngrxPush as pkg">
<ion-content class="ion-padding-top">
<ion-grid *ngIf="loading; else loaded" style="height: 100%;">
<ion-row class="ion-align-items-center ion-text-center" style="height: 100%;">
@@ -32,7 +32,7 @@
<ion-label class="ion-text-wrap">
<p><ion-text color="dark">About</ion-text></p>
<p>
Select a location from which to restore {{ pkg.installed.manifest.title }}. This will overwrite all current data.
Select a location from which to restore {{ title }}. This will overwrite all current data.
</p>
</ion-label>
</ion-item>

View File

@@ -5,6 +5,7 @@ import { BackupConfirmationComponent } from 'src/app/modals/backup-confirmation/
import { DiskInfo, PartitionInfoEntry } from 'src/app/services/api/api-types'
import { ActivatedRoute } from '@angular/router'
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
import { Subscription } from 'rxjs'
@Component({
selector: 'app-restore',
@@ -14,30 +15,44 @@ import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
export class AppRestorePage {
disks: DiskInfo
pkgId: string
title: string
loading = true
error: string
allPartitionsMounted: boolean
@ViewChild(IonContent) content: IonContent
subs: Subscription[] = []
constructor (
private readonly route: ActivatedRoute,
private readonly modalCtrl: ModalController,
private readonly apiService: ApiService,
private readonly loadingCtrl: LoadingController,
public readonly patch: PatchDbModel,
private readonly patch: PatchDbModel,
) { }
ngOnInit () {
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
this.subs = [
this.patch.watch$('package-data', this.pkgId, 'installed', 'manifest', 'title')
.subscribe(title => {
this.title = title
}),
]
this.getExternalDisks()
}
async ngAfterViewInit () {
ngAfterViewInit () {
this.content.scrollToPoint(undefined, 1)
}
async doRefresh () {
ngOnDestroy () {
this.subs.forEach(sub => sub.unsubscribe())
}
async refresh () {
this.loading = true
await this.getExternalDisks()
}

View File

@@ -80,41 +80,38 @@
<ion-item-group class="ion-padding-bottom">
<!-- dependencies -->
<ng-container *ngIf="!(pkg.installed['current-dependencies'] | empty)">
<ion-card id="dependencies" class="dep-card">
<ion-card-header>
<ion-card-title>Dependencies</ion-card-title>
</ion-card-header>
<ion-card-content>
<!-- A current-dependency is a subset of the manifest.dependencies that is currently required as determined by the service config. -->
<div *ngFor="let dep of pkg.installed['current-dependencies'] | keyvalue">
<ion-item *ngrxLet="patch.watch$('package-data', dep.key) as localDep" class="dependency-item">
<ion-avatar slot="start" style="position: relative; height: 5vh; width: 5vh; margin: 0px;">
<div class="dep-badge" [class]="pkg.installed.status['dependency-errors'][dep.key] ? 'dep-issue' : 'dep-sat'"></div>
<ion-item-divider id="dependencies">Dependencies</ion-item-divider>
<!-- A current-dependency is a subset of the manifest.dependencies that is currently required as determined by the service config. -->
<ion-grid>
<ion-row>
<ion-col *ngFor="let dep of pkg.installed['current-dependencies'] | keyvalue" sizeSm="12" sizeMd="6">
<ion-item *ngrxLet="patch.watch$('package-data', dep.key) as localDep">
<ion-thumbnail slot="start">
<img [src]="localDep ? localDep['static-files'].icon : pkg.installed.status['dependency-errors'][dep.key]?.icon" />
</ion-avatar>
<ion-label class="ion-text-wrap" style="padding: 1vh; padding-left: 2vh">
<h4 style="font-family: 'Montserrat'">{{ localDep ? (localDep | manifest).title : pkg.installed.status['dependency-errors'][dep.key]?.title }}</h4>
<p style="font-size: small">{{ manifest.dependencies[dep.key].version | displayEmver }}</p>
<p style="padding-top: 2px; position: relative; font-style: italic; font-size: smaller"><ion-text [color]="pkg.installed.status['dependency-errors'][dep.key] ? 'warning' : 'success'">{{ pkg.installed.status['dependency-errors'][dep.key] ? pkg.installed.status['dependency-errors'][dep.key].type : 'satisfied' }}</ion-text></p>
</ion-thumbnail>
<ion-label class="ion-text-wrap">
<h2 style="font-family: 'Montserrat'">{{ localDep ? (localDep | manifest).title : pkg.installed.status['dependency-errors'][dep.key]?.title }}</h2>
<p>{{ manifest.dependencies[dep.key].version | displayEmver }}</p>
<p><ion-text [color]="pkg.installed.status['dependency-errors'][dep.key] ? 'warning' : 'success'">{{ pkg.installed.status['dependency-errors'][dep.key] ? pkg.installed.status['dependency-errors'][dep.key].type : 'satisfied' }}</ion-text></p>
</ion-label>
<ion-button *ngIf="!pkg.installed.status['dependency-errors'][dep.key] || (pkg.installed.status['dependency-errors'][dep.key] && [DependencyErrorType.InterfaceHealthChecksFailed, DependencyErrorType.HealthChecksFailed] | includes : pkg.installed.status['dependency-errors'][dep.key].type)" slot="end" size="small" [routerLink]="['/services', dep.key]" color="primary" fill="outline" style="font-size: x-small">
<ion-button *ngIf="!pkg.installed.status['dependency-errors'][dep.key] || (pkg.installed.status['dependency-errors'][dep.key] && [DependencyErrorType.InterfaceHealthChecksFailed, DependencyErrorType.HealthChecksFailed] | includes : pkg.installed.status['dependency-errors'][dep.key].type)" slot="end" size="small" color="dark" [routerLink]="['/services', dep.key]" fill="outline">
View
</ion-button>
<ng-container *ngIf="pkg.installed.status['dependency-errors'][dep.key]">
<ion-button *ngIf="!localDep" slot="end" size="small" (click)="fixDep('install', dep.key)" color="primary" fill="outline" style="font-size: x-small">
<ion-button *ngIf="!localDep" slot="end" size="small" color="dark" (click)="fixDep('install', dep.key)" fill="outline">
Install
</ion-button>
<ng-container *ngIf="localDep && localDep.state === PackageState.Installed">
<ion-button *ngIf="pkg.installed.status['dependency-errors'][dep.key].type === DependencyErrorType.NotRunning" slot="end" size="small" [routerLink]="['/services', dep.key]" color="primary" fill="outline" style="font-size: x-small">
<ion-button *ngIf="pkg.installed.status['dependency-errors'][dep.key].type === DependencyErrorType.NotRunning" slot="end" size="small" color="dark" [routerLink]="['/services', dep.key]" fill="outline">
Start
</ion-button>
<ion-button *ngIf="pkg.installed.status['dependency-errors'][dep.key].type === DependencyErrorType.IncorrectVersion" slot="end" size="small" (click)="fixDep('update', dep.key)" color="primary" fill="outline" style="font-size: x-small">
<ion-button *ngIf="pkg.installed.status['dependency-errors'][dep.key].type === DependencyErrorType.IncorrectVersion" slot="end" size="small" color="dark" (click)="fixDep('update', dep.key)" fill="outline">
Update
</ion-button>
<ion-button *ngIf="pkg.installed.status['dependency-errors'][dep.key].type === DependencyErrorType.ConfigUnsatisfied" slot="end" size="small" (click)="fixDep('configure', dep.key)" color="primary" fill="outline" style="font-size: x-small">
<ion-button *ngIf="pkg.installed.status['dependency-errors'][dep.key].type === DependencyErrorType.ConfigUnsatisfied" slot="end" size="small" color="dark" (click)="fixDep('configure', dep.key)" fill="outline">
Configure
</ion-button>
</ng-container>
@@ -122,12 +119,11 @@
<div *ngIf="localDep && localDep.state !== PackageState.Installed" slot="end" class="spinner">
<ion-spinner [color]="localDep.state === PackageState.Removing ? 'danger' : 'primary'" style="height: 3vh; width: 3vh" name="dots"></ion-spinner>
</div>
</ng-container>
</ng-container>
</ion-item>
</div>
</ion-card-content>
</ion-card>
</ion-col>
</ion-row>
</ion-grid>
</ng-container>
</ion-item-group>
</ng-container>

View File

@@ -40,13 +40,6 @@
border-color: #404040;
}
.dep-badge {
position: absolute; width: 2.5vh;
height: 2.5vh;
border-radius: 50px;
left: -1vh;
top: -1vh;
}
.dep-issue {
background: radial-gradient(var(--ion-color-warning) 40%, transparent)
@@ -55,3 +48,9 @@
.dep-sat {
background: radial-gradient(var(--ion-color-success) 40%, transparent)
}
ion-item-divider {
text-transform: uppercase;
margin-top: 22px;
font-weight: 400;
}

View File

@@ -27,14 +27,12 @@ export class AppShowPage {
buttons: Button[] = []
manifest: Manifest = { } as Manifest
connected: boolean
subs: Subscription[] = []
FeStatus = FEStatus
PackageState = PackageState
DependencyErrorType = DependencyErrorType
@ViewChild(IonContent) content: IonContent
subs: Subscription[] = []
constructor (
private readonly alertCtrl: AlertController,
@@ -61,7 +59,7 @@ export class AppShowPage {
this.setButtons()
}
async ngAfterViewInit () {
ngAfterViewInit () {
this.content.scrollToPoint(undefined, 1)
}

View File

@@ -10,7 +10,7 @@
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding-top" *ngrxLet="patch.watch$('package-data') as installedPkgs">
<ion-content class="ion-padding-top">
<ion-spinner *ngIf="pageLoading; else pageLoaded" class="center" name="lines" color="warning"></ion-spinner>
<ng-template #pageLoaded>
@@ -43,23 +43,41 @@
</ion-button>
</div>
<div *ngIf="pkgsLoading; else pkgsLoaded" style="margin-top: 64px;" class="ion-text-center">
<div *ngIf="pkgsLoading; else loaded" style="margin-top: 64px;" class="ion-text-center">
<ion-spinner name="lines" color="warning"></ion-spinner>
</div>
<ng-template #pkgsLoaded>
<ion-grid style="margin: 0 6px;">
<ng-template #loaded>
<ion-grid>
<ion-row>
<ion-col *ngFor="let pkg of pkgs" sizeXl="2" sizeLg="3" sizeMd="3" sizeSm="4" sizeXs="4">
<ion-card class="ion-text-center" style="margin: 0;" [routerLink]="['/marketplace', pkg.id]">
<img [src]="pkg.icon" style="width: 90%; margin-top: 8px;" />
<ion-card-header style="min-height: 70px; text-align: left;">
<ion-card-title>{{ pkg.title }}</ion-card-title>
</ion-card-header>
<ion-card-content style="text-align: left;">
{{ pkg.descriptionShort }}
</ion-card-content>
</ion-card>
<ion-col *ngFor="let pkg of pkgs" sizeSm="12" sizeMd="6">
<ion-item [routerLink]="['/marketplace', pkg.id]">
<ion-thumbnail slot="start">
<img [src]="pkg.icon" />
</ion-thumbnail>
<ion-label>
<h2 style="font-family: 'Montserrat';">{{ pkg.title }}</h2>
<p>{{ pkg.descriptionShort }}</p>
<ng-container *ngIf="installedPkgs[pkg.id] as pkgI">
<p *ngIf="pkgI.state === PackageState.Installed">
<ion-text *ngIf="(pkg.version | compareEmver : pkgI.installed.manifest.version) === 0" color="success">Installed</ion-text>
<ion-text *ngIf="(pkg.version | compareEmver : pkgI.installed.manifest.version) === 1" color="warning">Update Available</ion-text>
</p>
<p *ngIf="pkgI.state === PackageState.Installing" style="display: flex; flex-direction: row; align-items: center;">
<ion-text color="primary">Installing</ion-text>
<ion-spinner name="crescent" style="height: 10px; width: 15px; margin-left: 3px; margin-right: -4px;" color="primary"></ion-spinner>
</p>
<p *ngIf="pkgI.state === PackageState.Updating" style="display: flex; flex-direction: row; align-items: center;">
<ion-text color="primary">Updating</ion-text>
<ion-spinner name="crescent" style="height: 10px; width: 15px; margin-left: 3px; margin-right: -4px;" color="primary"></ion-spinner>
</p>
<p *ngIf="pkgI.state === PackageState.Removing" style="display: flex; flex-direction: row; align-items: center;">
<ion-text color="danger">Removing</ion-text>
<ion-spinner name="crescent" style="height: 10px; width: 15px; margin-left: 3px; margin-right: -4px;" color="danger"></ion-spinner>
</p>
</ng-container>
</ion-label>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>

View File

@@ -1,10 +1,3 @@
.beneath-title {
font-size: 12px;
font-style: italic;
font-family: 'Open Sans';
padding: 1px 0px 1.5px 0px;
}
.scrollable {
overflow: auto;
white-space: nowrap;

View File

@@ -5,7 +5,8 @@ import { wizardModal } from 'src/app/components/install-wizard/install-wizard.co
import { ModalController } from '@ionic/angular'
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
import { PackageState } from 'src/app/models/patch-db/data-model'
import { PackageDataEntry, PackageState } from 'src/app/models/patch-db/data-model'
import { Subscription } from 'rxjs'
@Component({
selector: 'marketplace-list',
@@ -23,6 +24,7 @@ export class MarketplaceListPage {
data: MarketplaceData
eos: MarketplaceEOS
pkgs: AvailablePreview[] = []
installedPkgs: { [id: string]: PackageDataEntry } = { }
PackageState = PackageState
@@ -30,14 +32,21 @@ export class MarketplaceListPage {
needInfinite = false
readonly perPage = 20
subs: Subscription[] = []
constructor (
private readonly apiService: ApiService,
private readonly modalCtrl: ModalController,
private readonly wizardBaker: WizardBaker,
public patch: PatchDbModel,
private readonly patch: PatchDbModel,
) { }
async ngOnInit () {
this.subs = [
this.patch.watch$('package-data')
.subscribe(pkgs => this.installedPkgs = pkgs),
]
try {
const [data, eos, pkgs] = await Promise.all([
this.apiService.getMarketplaceData({ }),
@@ -56,6 +65,10 @@ export class MarketplaceListPage {
}
}
ngOnDestroy () {
this.subs.forEach(sub => sub.unsubscribe())
}
async doInfinite (e: any): Promise<void> {
const pkgs = await this.getPkgs()
this.pkgs = this.pkgs.concat(pkgs)
@@ -65,6 +78,7 @@ export class MarketplaceListPage {
async search (e?: any): Promise<void> {
this.query = e.target.value || undefined
this.page = 1
this.pkgsLoading = true
this.pkgs = await this.getPkgs()
}
@@ -79,7 +93,6 @@ export class MarketplaceListPage {
}
private async getPkgs (): Promise<AvailablePreview[]> {
this.pkgsLoading = true
try {
const pkgs = await this.apiService.getAvailableList({
category: this.category,
@@ -100,6 +113,8 @@ export class MarketplaceListPage {
async switchCategory (category: string): Promise<void> {
this.category = category
this.pkgsLoading = true
this.page = 1
this.pkgs = await this.getPkgs()
}
}

View File

@@ -19,59 +19,57 @@
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
</ion-item>
<ng-container *ngrxLet="patch.watch$('package-data', pkgId) as localPkg">
<ion-grid>
<ion-row>
<ion-col sizeXl="9" sizeLg="9" sizeMd="9" sizeSm="12" sizeXs="12">
<div class="header">
<img [src]="pkg.icon" />
<div class="header-text">
<h1 class="header-title">{{ pkg.manifest.title }}</h1>
<p class="header-version">{{ pkg.manifest.version | displayEmver }}</p>
<div class="header-status">
<!-- no localPkg -->
<p *ngIf="!localPkg; else local">
<ion-text color="medium">Not Installed</ion-text>
<ion-grid>
<ion-row>
<ion-col sizeXl="9" sizeLg="9" sizeMd="9" sizeSm="12" sizeXs="12">
<div class="header">
<img [src]="pkg.icon" />
<div class="header-text">
<h1 class="header-title">{{ pkg.manifest.title }}</h1>
<p class="header-version">{{ pkg.manifest.version | displayEmver }}</p>
<div class="header-status">
<!-- no installedPkg -->
<p *ngIf="!installedPkg; else local">
<ion-text color="medium">Not Installed</ion-text>
</p>
<!-- installedPkg -->
<ng-template #local>
<p *ngIf="installedPkg.state !== PackageState.Installed; else installed">
<!-- installing, updating, removing -->
<ion-text [color]="installedPkg.state === PackageState.Removing ? 'danger' : 'primary'">{{ installedPkg.state }}</ion-text>
<ion-spinner class="dots dots-medium" name="dots" [color]="installedPkg.state === PackageState.Removing ? 'danger' : 'primary'"></ion-spinner>
</p>
<!-- localPkg -->
<ng-template #local>
<p *ngIf="localPkg.state !== PackageState.Installed; else installed">
<!-- installing, updating, removing -->
<ion-text [color]="localPkg.state === PackageState.Removing ? 'danger' : 'primary'">{{ localPkg.state }}</ion-text>
<ion-spinner class="dots dots-medium" name="dots" [color]="localPkg.state === PackageState.Removing ? 'danger' : 'primary'"></ion-spinner>
<!-- installed -->
<ng-template #installed>
<p>
<ion-text color="medium">Installed at {{ installedPkg.installed.manifest.version | displayEmver }}</ion-text>
</p>
<!-- installed -->
<ng-template #installed>
<p>
<ion-text color="medium">Installed at {{ localPkg.installed.manifest.version | displayEmver }}</ion-text>
</p>
</ng-template>
</ng-template>
</div>
</ng-template>
</div>
</div>
</ion-col>
<ion-col sizeXl="3" sizeLg="3" sizeMd="3" sizeSm="12" sizeXs="12" class="ion-align-self-center">
<!-- no localPkg -->
<ion-button *ngIf="!localPkg; else localPkg2" class="main-action-button" expand="block" (click)="install()">
Install
</ion-button>
<!-- localPkg -->
<ng-template #localPkg2>
<!-- not installing, updating, or removing -->
<ng-container *ngIf="localPkg.state === PackageState.Installed">
<ion-button *ngIf="(localPkg.installed.manifest.version | compareEmver : pkg.manifest.version) === -1" class="main-action-button" expand="block" (click)="update('update')">
Update
</ion-button>
<ion-button *ngIf="(localPkg.installed.manifest.version | compareEmver : pkg.manifest.version) === 1" class="main-action-button" expand="block" color="warning" (click)="update('downgrade')">
Downgrade
</ion-button>
</ng-container>
</ng-template>
</ion-col>
</ion-row>
</ion-grid>
</ng-container>
</div>
</ion-col>
<ion-col sizeXl="3" sizeLg="3" sizeMd="3" sizeSm="12" sizeXs="12" class="ion-align-self-center">
<!-- no installedPkg -->
<ion-button *ngIf="!installedPkg; else installedPkg2" class="main-action-button" expand="block" (click)="install()">
Install
</ion-button>
<!-- installedPkg -->
<ng-template #installedPkg2>
<!-- not installing, updating, or removing -->
<ng-container *ngIf="installedPkg.state === PackageState.Installed">
<ion-button *ngIf="(installedPkg.installed.manifest.version | compareEmver : pkg.manifest.version) === -1" class="main-action-button" expand="block" (click)="update('update')">
Update
</ion-button>
<ion-button *ngIf="(installedPkg.installed.manifest.version | compareEmver : pkg.manifest.version) === 1" class="main-action-button" expand="block" color="warning" (click)="update('downgrade')">
Downgrade
</ion-button>
</ng-container>
</ng-template>
</ion-col>
</ion-row>
</ion-grid>
<!-- recommendation -->
<ion-item *ngIf="rec && showRec" class="rec-item">
@@ -121,24 +119,20 @@
<ion-item-divider>Dependencies</ion-item-divider>
<ion-grid>
<ion-row>
<ion-col *ngFor="let dep of pkg.manifest.dependencies | keyvalue" sizeMd="6" sizeSm="12" sizeXs="12">
<ion-card *ngIf="!dep.value.optional" style="--background: #171717" [routerLink]="['/marketplace', dep.key]">
<ion-item color="transparent" lines="none">
<ion-avatar slot="start">
<img [src]="pkg['dependency-metadata'][dep.key].icon" />
</ion-avatar>
<ion-label class="ion-text-wrap">
<h2>
{{ pkg['dependency-metadata'][dep.key].title }}
<span *ngIf="dep.value.recommended"> (recommended)</span>
</h2>
<p style="font-size: small">{{ dep.value.version | displayEmver }}</p>
</ion-label>
</ion-item>
<ion-card-content>
{{ dep.value.description }}
</ion-card-content>
</ion-card>
<ion-col *ngFor="let dep of pkg.manifest.dependencies | keyvalue" sizeSm="12" sizeMd="6">
<ion-item *ngIf="!dep.value.optional" [routerLink]="['/marketplace', dep.key]">
<ion-thumbnail slot="start">
<img [src]="pkg['dependency-metadata'][dep.key].icon" />
</ion-thumbnail>
<ion-label class="ion-text-wrap">
<h2>
{{ pkg['dependency-metadata'][dep.key].title }}
<span *ngIf="dep.value.recommended"> (recommended)</span>
</h2>
<p style="font-size: small">{{ dep.value.version | displayEmver }}</p>
<p>{{ dep.value.description }}</p>
</ion-label>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>

View File

@@ -2,7 +2,7 @@
font-family: 'Montserrat';
padding: 2%;
img {
min-width: 16%;
min-width: 15%;
max-width: 18%;
}
.header-text {
@@ -10,12 +10,11 @@
display: inline-block;
vertical-align: top;
.header-title {
line-height: .65;
margin: 0 0 0 -2px;
font-size: calc(20px + 3vw)
}
.header-version {
padding: 12px 0;
padding: 4px 0 12px 0;
margin: 0;
font-size: calc(10px + 1vw)
}

View File

@@ -8,8 +8,9 @@ import { Emver } from 'src/app/services/emver.service'
import { displayEmver } from 'src/app/pipes/emver.pipe'
import { pauseFor } from 'src/app/util/misc.util'
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
import { PackageState } from 'src/app/models/patch-db/data-model'
import { PackageDataEntry, PackageState } from 'src/app/models/patch-db/data-model'
import { MarketplaceService } from '../marketplace.service'
import { Subscription } from 'rxjs'
@Component({
selector: 'marketplace-show',
@@ -19,12 +20,13 @@ import { MarketplaceService } from '../marketplace.service'
export class MarketplaceShowPage {
error = ''
pkgId: string
installedPkg: PackageDataEntry
PackageState = PackageState
rec: Recommendation | null = null
showRec = true
subs: Subscription[] = []
constructor (
private readonly route: ActivatedRoute,
private readonly alertCtrl: AlertController,
@@ -32,17 +34,30 @@ export class MarketplaceShowPage {
private readonly wizardBaker: WizardBaker,
private readonly navCtrl: NavController,
private readonly emver: Emver,
public readonly patch: PatchDbModel,
private readonly patch: PatchDbModel,
public marketplaceService: MarketplaceService,
) { }
async ngOnInit () {
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
this.rec = history.state && history.state.installRec as Recommendation
this.subs = [
this.patch.watch$('package-data', this.pkgId)
.subscribe(pkg => {
console.log(pkg)
this.installedPkg = pkg
}),
]
this.getPkg()
}
ngOnDestroy () {
this.subs.forEach(sub => sub.unsubscribe())
}
async getPkg (version?: string): Promise<void> {
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
try {
await this.marketplaceService.setPkg(this.pkgId, version)
} catch (e) {

View File

@@ -9,7 +9,7 @@
<ion-content class="ion-padding-top">
<ion-item-group *ngrxLet="patch.watch$('server-info') as server">
<ion-item-group>
<ion-item detail="true" button [routerLink]="['ssh-keys']">
<ion-label>SSH Keys</ion-label>
</ion-item>

View File

@@ -1,6 +1,8 @@
import { Component } from '@angular/core'
import { ServerConfigService } from 'src/app/services/server-config.service'
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
import { ServerInfo } from 'src/app/models/patch-db/data-model'
import { Subscription } from 'rxjs'
@Component({
selector: 'dev-options',
@@ -8,12 +10,27 @@ import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
styleUrls: ['./dev-options.page.scss'],
})
export class DevOptionsPage {
server: ServerInfo = { } as any
subs: Subscription[] = []
constructor (
private readonly serverConfigService: ServerConfigService,
public readonly patch: PatchDbModel,
private readonly patch: PatchDbModel,
) { }
ngOnInit () {
this.subs = [
this.patch.watch$('server-info')
.subscribe(server => {
this.server = server
}),
]
}
ngOnDestroy () {
this.subs.forEach(sub => sub.unsubscribe())
}
async presentModalValueEdit (key: string, current?: any): Promise<void> {
await this.serverConfigService.presentModalValueEdit(key, current)
}

View File

@@ -4,6 +4,11 @@
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>SSH Keys</ion-title>
<ion-buttons slot="end" class="ion-padding-end">
<ion-button (click)="presentModalAdd()">
<ion-icon slot="icon-only" name="add-outline"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
@@ -16,7 +21,7 @@
<ion-item-group>
<ion-item-divider>Saved Keys</ion-item-divider>
<ion-item *ngFor="let ssh of sshService.watch$() | ngrxPush | keyvalue : asIsOrder">
<ion-item *ngFor="let ssh of sshKeys | keyvalue : asIsOrder">
<ion-label class="ion-text-wrap">
{{ ssh.value.alg }} {{ ssh.key }} {{ ssh.value.hostname }}
</ion-label>
@@ -26,10 +31,4 @@
</ion-item>
</ion-item-group>
<ion-fab vertical="bottom" horizontal="end" slot="fixed">
<ion-fab-button (click)="presentModalAdd()" class="fab-button">
<ion-icon name="add"></ion-icon>
</ion-fab-button>
</ion-fab>
</ion-content>

View File

@@ -3,6 +3,8 @@ import { ServerConfigService } from 'src/app/services/server-config.service'
import { AlertController } from '@ionic/angular'
import { LoaderService } from 'src/app/services/loader.service'
import { SSHService } from './ssh.service'
import { Subscription } from 'rxjs'
import { SSHKeys } from 'src/app/services/api/api-types'
@Component({
selector: 'dev-ssh-keys',
@@ -12,18 +14,31 @@ import { SSHService } from './ssh.service'
export class DevSSHKeysPage {
error = ''
loading = true
sshKeys: SSHKeys
subs: Subscription[] = []
constructor (
private readonly loader: LoaderService,
private readonly serverConfigService: ServerConfigService,
private readonly alertCtrl: AlertController,
public readonly sshService: SSHService,
private readonly sshService: SSHService,
) { }
ngOnInit () {
this.sshService.getKeys().then(() => {
this.loading = false
})
async ngOnInit () {
await this.sshService.getKeys()
this.subs = [
this.sshService.watch$()
.subscribe(keys => {
this.sshKeys = keys
}),
]
this.loading = false
}
ngOnDestroy () {
this.subs.forEach(sub => sub.unsubscribe())
}
async presentModalAdd () {

View File

@@ -17,8 +17,9 @@
<h2>You can connect to your Embassy over your Local Area Network (LAN). This can be useful for achieving a faster experience, as well as a fallback in case the Tor network is experiencing issues.</h2>
</ion-label>
</ion-item>
<ion-item>
<ion-button slot="start" fill="clear" color="primary" [href]="docsUrl" target="_blank">View Instructions</ion-button>
<ion-item [href]="docsUrl" target="_blank" detail="false">
<ion-icon slot="start" name="list-outline"></ion-icon>
<ion-label>View Instructions</ion-label>
</ion-item>
<ng-container *ngIf="lanDisabled">
@@ -39,11 +40,9 @@
<h2>If you are having issues connecting to your Embassy over LAN, try refreshing the network by clicking the button below.</h2>
</ion-label>
</ion-item>
<ion-item>
<ion-button slot="start" fill="clear" (click)="refreshLAN()">
<ion-icon slot="start" name="refresh-outline"></ion-icon>
Refresh Network
</ion-button>
<ion-item button (click)="refreshLAN()" detail="false">
<ion-icon slot="start" name="refresh-outline"></ion-icon>
<ion-label>Refresh Network</ion-label>
</ion-item>
<ion-item-divider></ion-item-divider>
@@ -63,7 +62,7 @@
<ion-item>
<ion-label class="ion-text-wrap">
<h2>LAN Address</h2>
<p>https://{{ patch.watch$('server-info', 'lan-address') | ngrxPush }}</p>
<p>{{ lanAddress }}</p>
</ion-label>
<ion-button slot="end" fill="clear" (click)="copyLAN()">
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>

View File

@@ -5,6 +5,7 @@ import { ConfigService } from 'src/app/services/config.service'
import { LoaderService } from 'src/app/services/loader.service'
import { ApiService } from 'src/app/services/api/api.service'
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
import { Subscription } from 'rxjs'
@Component({
selector: 'lan',
@@ -19,13 +20,14 @@ export class LANPage {
NotTor: `For security reasons, you must setup LAN over a Tor connection. Please navigate to your Embassy Tor Address and try again.`,
}
readonly docsUrl = 'https://docs.start9.com/user-manual/general/lan-setup'
subs: Subscription[] = []
constructor (
private readonly toastCtrl: ToastController,
private readonly config: ConfigService,
private readonly loader: LoaderService,
private readonly apiService: ApiService,
public readonly patch: PatchDbModel,
private readonly patch: PatchDbModel,
) { }
ngOnInit () {
@@ -34,6 +36,16 @@ export class LANPage {
} else if (!this.config.isTor()) {
this.lanDisabled = LanSetupIssue.NOT_TOR
}
this.subs = [
this.patch.watch$('server-info', 'lan-address')
.subscribe(addr => {
this.lanAddress = `https://${addr}`
}),
]
}
ngOnDestroy () {
this.subs.forEach(sub => sub.unsubscribe())
}
async refreshLAN (): Promise<void> {

View File

@@ -9,7 +9,7 @@
<ion-content class="ion-padding-top">
<ion-item-group *ngrxLet="patch.watch$('ui') as ui">
<ion-item-group>
<ion-item button (click)="presentModalValueEdit('name', ui['server-name'])">
<ion-label>Embassy Name</ion-label>
<ion-note slot="end">{{ ui['server-name'] }}</ion-note>

View File

@@ -1,6 +1,8 @@
import { Component } from '@angular/core'
import { ServerConfigService } from 'src/app/services/server-config.service'
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
import { Subscription } from 'rxjs'
import { UIData } from 'src/app/models/patch-db/data-model'
@Component({
selector: 'preferences',
@@ -8,11 +10,27 @@ import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
styleUrls: ['./preferences.page.scss'],
})
export class PreferencesPage {
subs: Subscription[] = []
ui: UIData = { } as any
constructor (
private readonly serverConfigService: ServerConfigService,
public readonly patch: PatchDbModel,
private readonly patch: PatchDbModel,
) { }
ngOnInit () {
this.subs = [
this.patch.watch$('ui')
.subscribe(ui => {
this.ui = ui
}),
]
}
ngOnDestroy () {
this.subs.forEach(sub => sub.unsubscribe())
}
async presentModalValueEdit (key: string, current?: string): Promise<void> {
await this.serverConfigService.presentModalValueEdit(key, current)
}

View File

@@ -4,9 +4,9 @@
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>Create Backup</ion-title>
<ion-buttons slot="end">
<ion-button (click)="doRefresh()" color="primary">
<ion-icon slot="icon-only" name="reload-outline"></ion-icon>
<ion-buttons slot="end" class="ion-padding-end">
<ion-button (click)="doRefresh()">
<ion-icon slot="icon-only" name="refresh-outline"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>

View File

@@ -5,8 +5,8 @@
</ion-buttons>
<ion-title>Logs</ion-title>
<ion-buttons slot="end" class="ion-padding-end">
<ion-button (click)="getLogs()" color="primary">
<ion-icon name="refresh-outline"></ion-icon>
<ion-button (click)="getLogs()">
<ion-icon slot="icon-only" name="refresh-outline"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>

View File

@@ -1,6 +1,6 @@
<ion-header>
<ion-toolbar>
<ion-title>{{ patch.watch$('ui', 'server-name') | ngrxPush }}</ion-title>
<ion-title>{{ patch.watch$('ui', 'server-name') | async }}</ion-title>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>

View File

@@ -11,7 +11,7 @@
<ion-item-divider>Basic</ion-item-divider>
<ion-item-group *ngIf="patch.watch$('server-info') | ngrxPush as server">
<ion-item-group *ngIf="server">
<ion-item>
<ion-label>
<h2>Version</h2>

View File

@@ -2,6 +2,8 @@ import { Component } from '@angular/core'
import { ToastController } from '@ionic/angular'
import { copyToClipboard } from 'src/app/util/web.util'
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
import { ServerInfo } from 'src/app/models/patch-db/data-model'
import { Subscription } from 'rxjs'
@Component({
selector: 'server-specs',
@@ -9,12 +11,27 @@ import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
styleUrls: ['./server-specs.page.scss'],
})
export class ServerSpecsPage {
server: ServerInfo
subs: Subscription[] = []
constructor (
private readonly toastCtrl: ToastController,
public readonly patch: PatchDbModel,
private readonly patch: PatchDbModel,
) { }
ngOnInit () {
this.subs = [
this.patch.watch$('server-info')
.subscribe(server => {
this.server = server
}),
]
}
ngOnDestroy () {
this.subs.forEach(sub => sub.unsubscribe())
}
async copy (address: string) {
let message = ''
await copyToClipboard(address || '')

View File

@@ -4,6 +4,11 @@
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>WiFi Settings</ion-title>
<ion-buttons slot="end" class="ion-padding-end">
<ion-button [routerLink]="['add']">
<ion-icon slot="icon-only" name="add-outline"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
@@ -17,13 +22,11 @@
<ion-label class="ion-text-wrap">
<p style="padding-bottom: 6px;">About</p>
<h2>Embassy will automatically connect to available networks, allowing you to remove the Ethernet cable.</h2>
<br />
<h2>Connecting, disconnecting, or changing WiFi networks can cause your Embassy and its services to become unreachable for up to an hour. Please be patient.</h2>
</ion-label>
</ion-item>
<ion-item-divider>Saved Networks</ion-item-divider>
<ng-container *ngIf="patch.watch$('server-info', 'wifi') | ngrxPush as wifi">
<ng-container *ngIf="wifi">
<ion-item button detail="false" *ngFor="let ssid of wifi.ssids" (click)="presentAction(ssid, wifi)">
<ion-label>{{ ssid }}</ion-label>
<ion-icon *ngIf="ssid === wifi.connected" name="wifi" color="success"></ion-icon>
@@ -31,10 +34,4 @@
</ng-container>
</ion-item-group>
<ion-fab vertical="bottom" horizontal="end" slot="fixed">
<ion-fab-button [routerLink]="['add']" class="fab-button">
<ion-icon name="add"></ion-icon>
</ion-fab-button>
</ion-fab>
</ion-content>

View File

@@ -6,6 +6,7 @@ import { WifiService } from './wifi.service'
import { LoaderService } from 'src/app/services/loader.service'
import { WiFiInfo } from 'src/app/models/patch-db/data-model'
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
import { Subscription } from 'rxjs'
@Component({
selector: 'wifi',
@@ -14,15 +15,30 @@ import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
})
export class WifiListPage {
error = ''
wifi: WiFiInfo
subs: Subscription[] = []
constructor (
private readonly apiService: ApiService,
private readonly loader: LoaderService,
private readonly actionCtrl: ActionSheetController,
private readonly wifiService: WifiService,
public readonly patch: PatchDbModel,
private readonly patch: PatchDbModel,
) { }
ngOnInit () {
this.subs = [
this.patch.watch$('server-info', 'wifi')
.subscribe(wifi => {
this.wifi = wifi
}),
]
}
ngOnDestroy () {
this.subs.forEach(sub => sub.unsubscribe())
}
async presentAction (ssid: string, wifi: WiFiInfo) {
const buttons: ActionSheetButton[] = [
{

View File

@@ -57,7 +57,7 @@ export class WifiService {
},
},
],
cssClass: 'notification-toast-error',
cssClass: 'notification-toast',
})
await toast.present()

View File

@@ -1,45 +0,0 @@
// import { Pipe, PipeTransform } from '@angular/core'
// import { combineLatest, Observable } from 'rxjs'
// import { map } from 'rxjs/operators'
// import { Emver } from '../services/emver.service'
// @Pipe({
// name: 'compareInstalledAndLatest',
// })
// export class InstalledLatestComparisonPipe implements PipeTransform {
// constructor (private readonly emver: Emver) { }
// transform (app: PropertySubject<AppAvailablePreview>): Observable<'not-installed' | 'installed-below' | 'installed-above' | 'installed-equal'> {
// return combineLatest([app.versionInstalled, app.versionLatest]).pipe(
// map(([i, l]) => {
// if (!i) return 'not-installed'
// switch (this.emver.compare(i, l)){
// case 0: return 'installed-equal'
// case 1: return 'installed-above'
// case -1: return 'installed-below'
// }
// }),
// )
// }
// }
// @Pipe({
// name: 'compareInstalledAndViewing',
// })
// export class InstalledViewingComparisonPipe implements PipeTransform {
// constructor (private readonly emver: Emver) { }
// transform (app: PropertySubject<AppAvailableFull>): Observable<'not-installed' | 'installed-below' | 'installed-above' | 'installed-equal'> {
// return combineLatest([app.versionInstalled, app.versionViewing]).pipe(
// map(([i, l]) => {
// if (!i) return 'not-installed'
// switch (this.emver.compare(i, l)){
// case 0: return 'installed-equal'
// case 1: return 'installed-above'
// case -1: return 'installed-below'
// }
// }),
// )
// }
// }

View File

@@ -5,5 +5,5 @@ import { Injectable } from '@angular/core'
providedIn: 'root',
})
export class SplitPaneTracker {
menuFixedOpenOnLeft$: BehaviorSubject<boolean> = new BehaviorSubject(false)
sidebarOpen$: BehaviorSubject<boolean> = new BehaviorSubject(false)
}

View File

@@ -58,14 +58,6 @@
border-style: none;
}
.fab-button {
margin: 20px;
--background: transparent;
--border-color: var(--ion-color-primary);
--border-style: solid;
--border-width: 2px;
}
.help-button {
margin: 0 8px 0 0;
}
@@ -74,12 +66,6 @@
--highlight-background: transparent !important;
}
.alert-config-value {
.alert-message.sc-ion-alert-md {
color: var(--ion-color-danger) !important;
}
}
.center {
display: block;
margin: auto;
@@ -95,15 +81,6 @@
--color: white;
}
.notification-toast-error {
--background: var(--ion-color-light);
--button-color: var(--ion-color-dark);
--border-color: var(--ion-color-danger);
--border-style: solid;
--border-width: 1px;
--color: white;
}
.sublist-spinner {
text-align: center;
margin-top: 40px;
@@ -315,8 +292,3 @@ ion-item-divider {
width: 16px !important;
height: 16px !important;
}
.dependency-item {
--padding-start: 20px;
--padding-end: 2px;
}