mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
refactor: completely remove ionic
This commit is contained in:
140
web/projects/ui/src/app/services/actions.service.ts
Normal file
140
web/projects/ui/src/app/services/actions.service.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core'
|
||||
import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit'
|
||||
import { defaultIfEmpty, filter, firstValueFrom } from 'rxjs'
|
||||
import {
|
||||
PackageConfigData,
|
||||
ConfigModal,
|
||||
} from 'src/app/routes/portal/modals/config.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { hasCurrentDeps } from 'src/app/utils/has-deps'
|
||||
import { getAllPackages } from 'src/app/utils/get-package-data'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { Manifest } from '../../../../../../core/startos/bindings/Manifest'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ActionsService {
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly patch = inject(PatchDB<DataModel>)
|
||||
|
||||
configure(manifest: Manifest): void {
|
||||
this.formDialog.open<PackageConfigData>(ConfigModal, {
|
||||
label: `${manifest.title} configuration`,
|
||||
data: { pkgId: manifest.id },
|
||||
})
|
||||
}
|
||||
|
||||
async start(manifest: Manifest, unmet: boolean): Promise<void> {
|
||||
const deps = `${manifest.title} has unmet dependencies. It will not work as expected.`
|
||||
|
||||
if (
|
||||
(!unmet || (await this.alert(deps))) &&
|
||||
(!manifest.alerts.start || (await this.alert(manifest.alerts.start)))
|
||||
) {
|
||||
this.doStart(manifest.id)
|
||||
}
|
||||
}
|
||||
|
||||
async stop({ id, title, alerts }: Manifest): Promise<void> {
|
||||
let content = alerts.stop || ''
|
||||
|
||||
if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
|
||||
const depMessage = `Services that depend on ${title} will no longer work properly and may crash`
|
||||
content = content ? `${content}.\n\n${depMessage}` : depMessage
|
||||
}
|
||||
|
||||
if (content) {
|
||||
this.dialogs
|
||||
.open(TUI_PROMPT, getOptions(content, 'Stop'))
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(() => this.doStop(id))
|
||||
} else {
|
||||
this.doStop(id)
|
||||
}
|
||||
}
|
||||
|
||||
async restart({ id, title }: Manifest): Promise<void> {
|
||||
if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
|
||||
this.dialogs
|
||||
.open(
|
||||
TUI_PROMPT,
|
||||
getOptions(
|
||||
`Services that depend on ${title} may temporarily experiences issues`,
|
||||
'Restart',
|
||||
),
|
||||
)
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(() => this.doRestart(id))
|
||||
} else {
|
||||
this.doRestart(id)
|
||||
}
|
||||
}
|
||||
|
||||
private async doStart(id: string): Promise<void> {
|
||||
const loader = this.loader.open(`Starting...`).subscribe()
|
||||
|
||||
try {
|
||||
await this.api.startPackage({ id })
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private async doStop(id: string): Promise<void> {
|
||||
const loader = this.loader.open(`Stopping...`).subscribe()
|
||||
|
||||
try {
|
||||
await this.api.stopPackage({ id })
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private async doRestart(id: string): Promise<void> {
|
||||
const loader = this.loader.open(`Restarting...`).subscribe()
|
||||
|
||||
try {
|
||||
await this.api.restartPackage({ id })
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private alert(content: string): Promise<boolean> {
|
||||
return firstValueFrom(
|
||||
this.dialogs
|
||||
.open<boolean>(TUI_PROMPT, getOptions(content))
|
||||
.pipe(defaultIfEmpty(false)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function getOptions(
|
||||
content: string,
|
||||
yes = 'Continue',
|
||||
): Partial<TuiDialogOptions<TuiPromptData>> {
|
||||
return {
|
||||
label: 'Warning',
|
||||
size: 's',
|
||||
data: {
|
||||
content,
|
||||
yes,
|
||||
no: 'Cancel',
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from './api.types'
|
||||
import { DependencyMetadata, MarketplacePkg } from '@start9labs/marketplace'
|
||||
import { Log } from '@start9labs/shared'
|
||||
import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
import { CT, T, CB } from '@start9labs/start-sdk'
|
||||
|
||||
const BTC_ICON = '/assets/img/service-icons/bitcoind.svg'
|
||||
|
||||
@@ -18,7 +18,7 @@ import { AuthService } from '../auth.service'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { DataModel } from '../patch-db/data-model'
|
||||
import { PatchDB, pathFromArray, Update } from 'patch-db-client'
|
||||
import { getServerInfo } from 'src/app/util/get-server-info'
|
||||
import { getServerInfo } from 'src/app/utils/get-server-info'
|
||||
|
||||
@Injectable()
|
||||
export class LiveApiService extends ApiService {
|
||||
|
||||
92
web/projects/ui/src/app/services/badge.service.ts
Normal file
92
web/projects/ui/src/app/services/badge.service.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
combineLatest,
|
||||
EMPTY,
|
||||
filter,
|
||||
first,
|
||||
map,
|
||||
Observable,
|
||||
pairwise,
|
||||
shareReplay,
|
||||
startWith,
|
||||
switchMap,
|
||||
} from 'rxjs'
|
||||
import { EOSService } from 'src/app/services/eos.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class BadgeService {
|
||||
private readonly emver = inject(Emver)
|
||||
private readonly patch = inject(PatchDB<DataModel>)
|
||||
private readonly settings$ = combineLatest([
|
||||
this.patch.watch$('serverInfo', 'ntpSynced'),
|
||||
inject(EOSService).updateAvailable$,
|
||||
]).pipe(map(([synced, update]) => Number(!synced) + Number(update)))
|
||||
private readonly marketplace = inject(
|
||||
AbstractMarketplaceService,
|
||||
) as MarketplaceService
|
||||
|
||||
private readonly local$ = inject(ConnectionService).connected$.pipe(
|
||||
filter(Boolean),
|
||||
switchMap(() => this.patch.watch$('packageData').pipe(first())),
|
||||
switchMap(outer =>
|
||||
this.patch.watch$('packageData').pipe(
|
||||
pairwise(),
|
||||
filter(([prev, curr]) =>
|
||||
Object.values(prev).some(p => {
|
||||
const { id } = getManifest(p)
|
||||
|
||||
return (
|
||||
!curr[id] ||
|
||||
(p.stateInfo.installingInfo && !curr[id].stateInfo.installingInfo)
|
||||
)
|
||||
}),
|
||||
),
|
||||
map(([_, curr]) => curr),
|
||||
startWith(outer),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private readonly updates$ = combineLatest([
|
||||
this.marketplace.getMarketplace$(true),
|
||||
this.local$,
|
||||
]).pipe(
|
||||
map(
|
||||
([marketplace, local]) =>
|
||||
Object.entries(marketplace).reduce(
|
||||
(list, [_, store]) =>
|
||||
store?.packages.reduce(
|
||||
(result, { manifest: { id, version } }) =>
|
||||
local[id] &&
|
||||
this.emver.compare(version, getManifest(local[id]).version) ===
|
||||
1
|
||||
? result.add(id)
|
||||
: result,
|
||||
list,
|
||||
) || list,
|
||||
new Set<string>(),
|
||||
).size,
|
||||
),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
getCount(id: string): Observable<number> {
|
||||
switch (id) {
|
||||
case '/portal/system/updates':
|
||||
return this.updates$
|
||||
case '/portal/system/settings':
|
||||
return this.settings$
|
||||
default:
|
||||
return EMPTY
|
||||
}
|
||||
}
|
||||
}
|
||||
88
web/projects/ui/src/app/services/breadcrumbs.service.ts
Normal file
88
web/projects/ui/src/app/services/breadcrumbs.service.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { SYSTEM_UTILITIES } from 'src/app/utils/system-utilities'
|
||||
import { toRouterLink } from 'src/app/utils/to-router-link'
|
||||
import { getAllPackages, getManifest } from 'src/app/utils/get-package-data'
|
||||
|
||||
export interface Breadcrumb {
|
||||
title: string
|
||||
routerLink: string
|
||||
subtitle?: string
|
||||
icon?: string
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class BreadcrumbsService extends BehaviorSubject<readonly Breadcrumb[]> {
|
||||
private readonly patch = inject(PatchDB<DataModel>)
|
||||
|
||||
constructor() {
|
||||
super([])
|
||||
}
|
||||
|
||||
async update(page: string) {
|
||||
const packages = await getAllPackages(this.patch)
|
||||
|
||||
try {
|
||||
this.next(toBreadcrumbs(page.split('?')[0], packages))
|
||||
} catch (e) {
|
||||
this.next([])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toBreadcrumbs(
|
||||
id: string,
|
||||
packages: Record<string, PackageDataEntry> = {},
|
||||
): Breadcrumb[] {
|
||||
const item = SYSTEM_UTILITIES[id]
|
||||
const routerLink = toRouterLink(id)
|
||||
|
||||
if (id.startsWith('/portal/system/')) {
|
||||
const [page, ...path] = id.replace('/portal/system/', '').split('/')
|
||||
const service = `/portal/system/${page}`
|
||||
const { icon, title } = SYSTEM_UTILITIES[service]
|
||||
const breadcrumbs: Breadcrumb[] = [
|
||||
{
|
||||
icon,
|
||||
title,
|
||||
routerLink: toRouterLink(service),
|
||||
},
|
||||
]
|
||||
|
||||
if (path.length) {
|
||||
breadcrumbs.push({
|
||||
title: path.join(': '),
|
||||
routerLink: breadcrumbs[0].routerLink + '/' + path.join('/'),
|
||||
})
|
||||
}
|
||||
|
||||
return breadcrumbs
|
||||
}
|
||||
|
||||
const [service, ...path] = id.split('/')
|
||||
const { title, version } = getManifest(packages[service])
|
||||
const breadcrumbs: Breadcrumb[] = [
|
||||
{
|
||||
icon: packages[service].icon,
|
||||
title,
|
||||
subtitle: version,
|
||||
routerLink: toRouterLink(service),
|
||||
},
|
||||
]
|
||||
|
||||
if (path.length) {
|
||||
breadcrumbs.push({
|
||||
title: path.join(': '),
|
||||
routerLink: breadcrumbs[0].routerLink + '/' + path.join('/'),
|
||||
})
|
||||
}
|
||||
|
||||
return breadcrumbs
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from './patch-db/data-model'
|
||||
import * as deepEqual from 'fast-deep-equal'
|
||||
import { Observable } from 'rxjs'
|
||||
import { isInstalled } from '../util/get-package-data'
|
||||
import { isInstalled } from 'src/app/utils/get-package-data'
|
||||
import { DependencyError } from './api/api.types'
|
||||
|
||||
export type AllDependencyErrors = Record<string, PkgDependencyErrors>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { PatchDB } from 'patch-db-client'
|
||||
import { BehaviorSubject, distinctUntilChanged, map, combineLatest } from 'rxjs'
|
||||
import { MarketplaceEOS } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { getServerInfo } from 'src/app/util/get-server-info'
|
||||
import { getServerInfo } from 'src/app/utils/get-server-info'
|
||||
import { DataModel } from './patch-db/data-model'
|
||||
|
||||
@Injectable({
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
ValidatorFn,
|
||||
Validators,
|
||||
} from '@angular/forms'
|
||||
import { getDefaultString } from '../util/config-utilities'
|
||||
import { getDefaultString } from 'src/app/utils/config-utilities'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
const Mustache = require('mustache')
|
||||
|
||||
|
||||
116
web/projects/ui/src/app/services/notification.service.ts
Normal file
116
web/projects/ui/src/app/services/notification.service.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { ErrorService } from '@start9labs/shared'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import {
|
||||
NotificationLevel,
|
||||
ServerNotification,
|
||||
ServerNotifications,
|
||||
} from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { REPORT } from 'src/app/components/report.component'
|
||||
import { firstValueFrom, merge, shareReplay, Subject } from 'rxjs'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class NotificationService {
|
||||
private readonly patch = inject(PatchDB<DataModel>)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
private readonly localUnreadCount$ = new Subject<number>()
|
||||
|
||||
readonly unreadCount$ = merge(
|
||||
this.patch.watch$('serverInfo', 'unreadNotifications', 'count'),
|
||||
this.localUnreadCount$,
|
||||
).pipe(shareReplay(1))
|
||||
|
||||
async markSeen(notifications: ServerNotifications) {
|
||||
const ids = notifications.filter(n => !n.read).map(n => n.id)
|
||||
|
||||
this.updateCount(-ids.length)
|
||||
|
||||
this.api
|
||||
.markSeenNotifications({ ids })
|
||||
.catch(e => this.errorService.handleError(e))
|
||||
}
|
||||
|
||||
async markSeenAll(latestId: number) {
|
||||
this.localUnreadCount$.next(0)
|
||||
|
||||
this.api
|
||||
.markSeenAllNotifications({ before: latestId })
|
||||
.catch(e => this.errorService.handleError(e))
|
||||
}
|
||||
|
||||
async markUnseen(notifications: ServerNotifications) {
|
||||
const ids = notifications.filter(n => n.read).map(n => n.id)
|
||||
|
||||
this.updateCount(ids.length)
|
||||
|
||||
this.api
|
||||
.markUnseenNotifications({ ids })
|
||||
.catch(e => this.errorService.handleError(e))
|
||||
}
|
||||
|
||||
async remove(notifications: ServerNotifications): Promise<void> {
|
||||
this.updateCount(-notifications.filter(n => !n.read).length)
|
||||
|
||||
this.api
|
||||
.deleteNotifications({ ids: notifications.map(n => n.id) })
|
||||
.catch(e => this.errorService.handleError(e))
|
||||
}
|
||||
|
||||
getColor(notification: ServerNotification<number>): string {
|
||||
switch (notification.level) {
|
||||
case NotificationLevel.Info:
|
||||
return 'var(--tui-info-fill)'
|
||||
case NotificationLevel.Success:
|
||||
return 'var(--tui-success-fill)'
|
||||
case NotificationLevel.Warning:
|
||||
return 'var(--tui-warning-fill)'
|
||||
case NotificationLevel.Error:
|
||||
return 'var(--tui-error-fill)'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
getIcon(notification: ServerNotification<number>): string {
|
||||
switch (notification.level) {
|
||||
case NotificationLevel.Info:
|
||||
return 'tuiIconInfo'
|
||||
case NotificationLevel.Success:
|
||||
return 'tuiIconCheckCircle'
|
||||
case NotificationLevel.Warning:
|
||||
case NotificationLevel.Error:
|
||||
return 'tuiIconAlertCircle'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
viewFull(notification: ServerNotification<number>) {
|
||||
this.dialogs
|
||||
.open(notification.message, { label: notification.title })
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
viewReport(notification: ServerNotification<number>) {
|
||||
this.dialogs
|
||||
.open(REPORT, {
|
||||
label: 'Backup Report',
|
||||
data: {
|
||||
report: notification.data,
|
||||
timestamp: notification.createdAt,
|
||||
},
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
private async updateCount(toAdjust: number) {
|
||||
const currentCount = await firstValueFrom(this.unreadCount$)
|
||||
|
||||
this.localUnreadCount$.next(Math.max(currentCount + toAdjust, 0))
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { filter, share, switchMap, take, tap, Observable } from 'rxjs'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { EOSService } from 'src/app/services/eos.service'
|
||||
import { OSWelcomePage } from '../common/os-welcome/os-welcome.page'
|
||||
import { WelcomeComponent } from 'src/app/components/welcome.component'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
@@ -54,7 +54,7 @@ export class PatchDataService extends Observable<DataModel> {
|
||||
}
|
||||
|
||||
this.dialogs
|
||||
.open(new PolymorpheusComponent(OSWelcomePage), {
|
||||
.open(new PolymorpheusComponent(WelcomeComponent), {
|
||||
label: 'Release Notes',
|
||||
})
|
||||
.subscribe({
|
||||
|
||||
@@ -6,9 +6,9 @@ import { firstValueFrom } from 'rxjs'
|
||||
import {
|
||||
FormComponent,
|
||||
FormContext,
|
||||
} from 'src/app/apps/portal/components/form.component'
|
||||
} from 'src/app/routes/portal/components/form.component'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
import { ApiService } from './api/embassy-api.service'
|
||||
import { DataModel } from './patch-db/data-model'
|
||||
import { CB } from '@start9labs/start-sdk'
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { BehaviorSubject, Observable } from 'rxjs'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SidebarService {
|
||||
openMap: Record<string, BehaviorSubject<boolean>> = {}
|
||||
|
||||
setMap(ids: string[]) {
|
||||
ids.map(i => (this.openMap[i] = new BehaviorSubject(false)))
|
||||
}
|
||||
|
||||
getToggleState(pkgId: string): Observable<boolean> {
|
||||
return this.openMap[pkgId]
|
||||
}
|
||||
|
||||
toggleState(pkgId: string, open: boolean) {
|
||||
this.openMap[pkgId].next(open)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { Injectable } from '@angular/core'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SplitPaneTracker {
|
||||
readonly sidebarOpen$ = new BehaviorSubject<boolean>(false)
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { map, shareReplay, startWith, switchMap } from 'rxjs/operators'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel } from './patch-db/data-model'
|
||||
import { ApiService } from './api/embassy-api.service'
|
||||
import { combineLatest, interval, of } from 'rxjs'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class TimeService {
|
||||
private readonly time$ = of({}).pipe(
|
||||
switchMap(() => this.apiService.getSystemTime({})),
|
||||
switchMap(({ now, uptime }) => {
|
||||
const current = new Date(now).valueOf()
|
||||
return interval(1000).pipe(
|
||||
map(index => {
|
||||
const incremented = index + 1
|
||||
return {
|
||||
now: current + 1000 * incremented,
|
||||
uptime: uptime + incremented,
|
||||
}
|
||||
}),
|
||||
startWith({
|
||||
now: current,
|
||||
uptime,
|
||||
}),
|
||||
)
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
)
|
||||
|
||||
readonly now$ = combineLatest([
|
||||
this.time$,
|
||||
this.patch.watch$('serverInfo', 'ntpSynced'),
|
||||
]).pipe(
|
||||
map(([time, synced]) => ({
|
||||
value: time.now,
|
||||
synced,
|
||||
})),
|
||||
)
|
||||
|
||||
readonly uptime$ = this.time$.pipe(
|
||||
map(({ uptime }) => {
|
||||
const days = Math.floor(uptime / (24 * 60 * 60))
|
||||
const daysSec = uptime % (24 * 60 * 60)
|
||||
const hours = Math.floor(daysSec / (60 * 60))
|
||||
const hoursSec = uptime % (60 * 60)
|
||||
const minutes = Math.floor(hoursSec / 60)
|
||||
const seconds = uptime % 60
|
||||
return { days, hours, minutes, seconds }
|
||||
}),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly apiService: ApiService,
|
||||
) {}
|
||||
}
|
||||
Reference in New Issue
Block a user