Bugfix/040 UI (#2881)

* fix sideload and install flow

* move updates chevron inside upddate button

* update dictionaries to include langauge names

* fix: address todos (#2880)

* fix: address todos

* fix enlgish translation

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>

* use existing translation, no need to duplicate

* fix: update dialog and other fixes (#2882)

---------

Co-authored-by: Alex Inkin <alexander@inkin.ru>
Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
Matt Hill
2025-04-21 10:57:12 -06:00
committed by GitHub
parent b1621f6b34
commit 27272680a2
59 changed files with 515 additions and 510 deletions

View File

@@ -92,7 +92,24 @@ cp proxy.conf-sample.json proxy.conf.json
npm run start:ui:proxy npm run start:ui:proxy
``` ```
## Updating translations ## Translations
### Currently supported languages
- Spanish
- Polish
- German
<!-- - Korean
- Russian
- Japanese
- Hebrew
- Arabic
- Mandarin
- Hindi
- Portuguese
- French
- Italian
- Thai -->
### Adding a new translation ### Adding a new translation
@@ -119,22 +136,20 @@ Translate the English dictionary below into `<language>`. Format the result as a
#### Sample AI prompt #### Sample AI prompt
Translate `<original>` into the languages below. Return the translations as a JSON object with the languages as keys. Translate the English dictionary below into the languages beneath the dictionary. Format the result as a javascript object with translated language as keys, mapping to a javascript object with the numeric values of the English dictionary as keys and the translations as values. These translations are for the web UI of StartOS, a graphical server operating system optimized for self-hosting. Comments may be included in the English dictionary to provide additional context.
English dictionary:
```
'Hello': 420,
'Goodby': 421
```
Languages:
- Spanish - Spanish
- Polish - Polish
- German - German
<!-- - Korean
- Russian
- Japanese
- Hebrew
- Arabic
- Mandarin
- Hindi
- Portuguese
- French
- Italian
- Thai -->
#### Adding to StartOS #### Adding to StartOS

View File

@@ -21,14 +21,7 @@
[(query)]="query" [(query)]="query"
(queryChange)="onQueryChange($event)" (queryChange)="onQueryChange($event)"
/> />
<button <button tuiButton type="button" appearance="" (click)="open.set(true)">
tuiButton
type="button"
appearance="link"
(click)="toggleMenu(true)"
(tuiActiveZoneChange)="toggleMenu($event)"
[style.--tui-padding]="'1.2rem'"
>
<store-icon <store-icon
size="42px" size="42px"
[style.height.px]="42" [style.height.px]="42"
@@ -37,46 +30,47 @@
[marketplace]="iconConfig" [marketplace]="iconConfig"
[tuiSkeleton]="!registry" [tuiSkeleton]="!registry"
/> />
<nav <tui-drawer
*tuiSidebar="open; direction: 'right'; autoWidth: true" *tuiPopup="open()"
class="nav-mobile-sidebar divide-bar" [overlay]="true"
(click.self)="open.set(false)"
> >
<div class="nav-mobile-sidebar-top"> <nav class="nav-mobile-sidebar divide-bar">
<h1 [tuiSkeleton]="!registry"> <div class="nav-mobile-sidebar-top">
{{ registry?.info?.name }} <h1 [style.margin]="0" [tuiSkeleton]="!registry">
</h1> {{ registry?.info?.name }}
<button </h1>
[style.--tui-padding]="0" <button
tuiButton tuiButton
type="button" type="button"
appearance="icon" appearance="icon"
iconStart="@tui.x" iconStart="@tui.x"
(tuiActiveZoneChange)="toggleMenu($event)" (click)="open.set(false)"
(click)="toggleMenu(false)" ></button>
></button>
</div>
<!-- change registry modal -->
<ng-content select="[slot=mobile]"></ng-content>
<div class="nav-mobile-sidebar-bottom divide-bar">
<marketplace-categories
[categories]="registry?.info?.categories"
[category]="query ? '' : category"
(categoryChange)="onCategoryChange($event); toggleMenu(false)"
/>
<div>
<!-- link to store for brochure -->
<ng-content select="[slot=store-mobile]" />
<a
target="_blank"
rel="noreferrer"
href="https://docs.start9.com/0.3.5.x/developer-docs/"
>
<span>Package a service</span>
<tui-icon tuiAppearance="icon" icon="@tui.external-link" />
</a>
</div> </div>
</div> <!-- change registry modal -->
</nav> <ng-content select="[slot=mobile]"></ng-content>
<div class="nav-mobile-sidebar-bottom divide-bar">
<marketplace-categories
[categories]="registry?.info?.categories"
[category]="query ? '' : category"
(categoryChange)="onCategoryChange($event); open.set(false)"
/>
<div>
<!-- link to store for brochure -->
<ng-content select="[slot=store-mobile]" />
<a
target="_blank"
rel="noreferrer"
href="https://docs.start9.com/0.3.5.x/developer-docs/"
>
<span>Package a service</span>
<tui-icon tuiAppearance="icon" icon="@tui.external-link" />
</a>
</div>
</div>
</nav>
</tui-drawer>
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,15 +1,19 @@
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { SharedPipesModule } from '@start9labs/shared' import { SharedPipesModule } from '@start9labs/shared'
import { TuiSkeleton } from '@taiga-ui/kit' import { TuiLet } from '@taiga-ui/cdk'
import {
import { MenuComponent } from './menu.component' TuiAppearance,
import { TuiLoader, TuiIcon, TuiButton, TuiAppearance } from '@taiga-ui/core' TuiButton,
import { TuiActiveZone, TuiLet } from '@taiga-ui/cdk' TuiIcon,
import { TuiSidebar } from '@taiga-ui/addon-mobile' TuiLoader,
import { SearchModule } from '../../pages/list/search/search.module' TuiPopup,
} from '@taiga-ui/core'
import { TuiDrawer, TuiSkeleton } from '@taiga-ui/kit'
import { CategoriesModule } from '../../pages/list/categories/categories.module' import { CategoriesModule } from '../../pages/list/categories/categories.module'
import { SearchModule } from '../../pages/list/search/search.module'
import { StoreIconComponentModule } from '../store-icon/store-icon.component.module' import { StoreIconComponentModule } from '../store-icon/store-icon.component.module'
import { MenuComponent } from './menu.component'
@NgModule({ @NgModule({
imports: [ imports: [
@@ -17,8 +21,6 @@ import { StoreIconComponentModule } from '../store-icon/store-icon.component.mod
SharedPipesModule, SharedPipesModule,
SearchModule, SearchModule,
CategoriesModule, CategoriesModule,
TuiActiveZone,
...TuiSidebar,
TuiLoader, TuiLoader,
TuiButton, TuiButton,
CategoriesModule, CategoriesModule,
@@ -27,6 +29,8 @@ import { StoreIconComponentModule } from '../store-icon/store-icon.component.mod
TuiAppearance, TuiAppearance,
TuiIcon, TuiIcon,
TuiSkeleton, TuiSkeleton,
TuiDrawer,
TuiPopup,
], ],
declarations: [MenuComponent], declarations: [MenuComponent],
exports: [MenuComponent], exports: [MenuComponent],

View File

@@ -5,6 +5,12 @@
padding: 0; padding: 0;
} }
tui-drawer {
top: 0;
border-radius: 0;
background-color: rgb(var(--tw-color-zinc-700) / 0.9);
}
header { header {
@include scrollbar-hidden(); @include scrollbar-hidden();
@@ -131,12 +137,10 @@ header {
} }
&-sidebar { &-sidebar {
background-color: rgb(var(--tw-color-zinc-700) / 0.9);
width: 70vw;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
overflow: auto; margin: -1.25rem -1.5rem;
&-top { &-top {
display: flex; display: flex;
@@ -166,7 +170,7 @@ header {
marketplace-categories { marketplace-categories {
flex-grow: 1; flex-grow: 1;
padding: 1.25rem 1.25rem 0px 1.25rem; padding: 1.25rem 1.25rem 0 1.25rem;
} }
::ng-deep a { ::ng-deep a {
@@ -191,7 +195,7 @@ header {
.divide-bar > * + * { .divide-bar > * + * {
border-top-width: 1px; border-top-width: 1px;
border-bottom-width: 0px; border-bottom-width: 0;
border-color: rgb(113 113 122); border-color: rgb(113 113 122);
} }
} }

View File

@@ -4,6 +4,7 @@ import {
inject, inject,
Input, Input,
OnDestroy, OnDestroy,
signal,
} from '@angular/core' } from '@angular/core'
import { MarketplaceConfig } from '@start9labs/shared' import { MarketplaceConfig } from '@start9labs/shared'
import { Subject, takeUntil } from 'rxjs' import { Subject, takeUntil } from 'rxjs'
@@ -27,7 +28,7 @@ export class MenuComponent implements OnDestroy {
private readonly categoryService = inject(AbstractCategoryService) private readonly categoryService = inject(AbstractCategoryService)
category = '' category = ''
query = '' query = ''
open = false readonly open = signal(false)
ngOnInit() { ngOnInit() {
this.categoryService this.categoryService
@@ -57,10 +58,6 @@ export class MenuComponent implements OnDestroy {
this.categoryService.setQuery(query) this.categoryService.setQuery(query)
} }
toggleMenu(open: boolean): void {
this.open = open
}
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroy$.next() this.destroy$.next()
this.destroy$.complete() this.destroy$.complete()

View File

@@ -1,29 +1,29 @@
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { TuiIcon, TuiTitle } from '@taiga-ui/core' import { TuiIcon, TuiTitle } from '@taiga-ui/core'
import { TuiLineClamp } from '@taiga-ui/kit' import { TuiFade } from '@taiga-ui/kit'
@Component({ @Component({
selector: 'marketplace-additional-item', selector: 'marketplace-additional-item',
template: ` template: `
<div class="item-container"> <label tuiTitle>
<label tuiTitle> <span tuiSubtitle>{{ label }}</span>
<span tuiSubtitle>{{ label }}</span> <span tuiFade>{{ data }}</span>
<tui-line-clamp [content]="data" [linesLimit]="1" /> </label>
</label> <tui-icon [icon]="icon" />
<tui-icon [icon]="icon" />
</div>
`, `,
styles: [ styles: [
` `
.item-container { :host {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
gap: 0.5rem;
padding: 0.75rem 0.25rem; padding: 0.75rem 0.25rem;
white-space: nowrap;
&:hover { &:hover {
background-color: rgb(113 113 122 / 0.1); background-color: var(--tui-background-neutral-1);
} }
[tuiSubtitle] { [tuiSubtitle] {
@@ -34,16 +34,11 @@ import { TuiLineClamp } from '@taiga-ui/kit'
opacity: 0.7; opacity: 0.7;
} }
} }
::ng-deep .t-text {
font-family: 'Montserrat';
font-weight: 600;
}
`, `,
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true, standalone: true,
imports: [CommonModule, TuiLineClamp, TuiIcon, TuiTitle], imports: [CommonModule, TuiIcon, TuiTitle, TuiFade],
}) })
export class MarketplaceAdditionalItemComponent { export class MarketplaceAdditionalItemComponent {
@Input({ required: true }) @Input({ required: true })

View File

@@ -7,6 +7,7 @@
.detail-container { .detail-container {
display: grid; display: grid;
grid-auto-flow: row; grid-auto-flow: row;
grid-auto-columns: minmax(0, 1fr);
& > * + * { & > * + * {
border-top-width: 1px; border-top-width: 1px;

View File

@@ -4,7 +4,7 @@ import { ActivatedRoute, Data } from '@angular/router'
import { TuiDialogContext, TuiLoader, TuiNotification } from '@taiga-ui/core' import { TuiDialogContext, TuiLoader, TuiNotification } from '@taiga-ui/core'
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus' import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { NgDompurifyModule } from '@tinkoff/ng-dompurify' import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
import { catchError, ignoreElements, of } from 'rxjs' import { catchError, ignoreElements, Observable, of } from 'rxjs'
import { SafeLinksDirective } from '../directives/safe-links.directive' import { SafeLinksDirective } from '../directives/safe-links.directive'
import { MarkdownPipe } from '../pipes/markdown.pipe' import { MarkdownPipe } from '../pipes/markdown.pipe'
import { getErrorMessage } from '../services/error.service' import { getErrorMessage } from '../services/error.service'
@@ -36,8 +36,9 @@ import { getErrorMessage } from '../services/error.service'
}) })
export class MarkdownComponent { export class MarkdownComponent {
private readonly data = private readonly data =
injectContext<TuiDialogContext<void, Data>>({ optional: true })?.data || injectContext<TuiDialogContext<void, { content: Observable<string> }>>({
inject(ActivatedRoute).snapshot.data optional: true,
})?.data || inject(ActivatedRoute).snapshot.data
readonly content = toSignal<string>(this.data['content']) readonly content = toSignal<string>(this.data['content'])
readonly error = toSignal( readonly error = toSignal(

View File

@@ -1,28 +1,39 @@
import { AfterViewInit, Directive, ElementRef, Inject } from '@angular/core'
import { DOCUMENT } from '@angular/common' import { DOCUMENT } from '@angular/common'
import { Directive, inject } from '@angular/core'
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
import {
MutationObserverService,
provideMutationObserverInit,
} from '@ng-web-apis/mutation-observer'
import { tuiInjectElement } from '@taiga-ui/cdk'
// @TODO Alex: Refactor to use `MutationObserver` so it works with dynamic content
@Directive({ @Directive({
selector: '[safeLinks]', selector: '[safeLinks]',
providers: [
MutationObserverService,
provideMutationObserverInit({
childList: true,
subtree: true,
}),
],
standalone: true, standalone: true,
}) })
export class SafeLinksDirective implements AfterViewInit { export class SafeLinksDirective {
constructor( private readonly doc = inject(DOCUMENT)
@Inject(DOCUMENT) private readonly document: Document, private readonly el = tuiInjectElement()
private readonly elementRef: ElementRef<HTMLElement>, private readonly sub = inject(MutationObserverService)
) {} .pipe(takeUntilDestroyed())
.subscribe(() => {
ngAfterViewInit() { Array.from(this.doc.links)
Array.from(this.document.links) .filter(
.filter( link =>
link => link.hostname !== this.doc.location.hostname &&
link.hostname !== this.document.location.hostname && this.el.contains(link),
this.elementRef.nativeElement.contains(link), )
) .forEach(link => {
.forEach(link => { link.target = '_blank'
link.target = '_blank' link.setAttribute('rel', 'noreferrer')
link.setAttribute('rel', 'noreferrer') link.classList.add('g-external-link')
link.classList.add('g-external-link') })
}) })
}
} }

View File

@@ -488,4 +488,8 @@ export default {
485: 'StartOS-Benutzeroberfläche', 485: 'StartOS-Benutzeroberfläche',
486: 'WiFi', 486: 'WiFi',
487: 'Anleitungen', 487: 'Anleitungen',
488: 'spanisch',
489: 'polnisch',
490: 'deutsch',
491: 'englisch',
} satisfies i18n } satisfies i18n

View File

@@ -487,4 +487,8 @@ export const ENGLISH = {
'StartOS UI': 485, 'StartOS UI': 485,
'WiFi': 486, 'WiFi': 486,
'Instructions': 487, 'Instructions': 487,
'spanish': 488,
'polish': 489,
'german': 490,
'english': 491,
} as const } as const

View File

@@ -488,4 +488,8 @@ export default {
485: 'Interfaz de StartOS', 485: 'Interfaz de StartOS',
486: 'WiFi', 486: 'WiFi',
487: 'Instrucciones', 487: 'Instrucciones',
488: 'español',
489: 'polaco',
490: 'alemán',
491: 'inglés',
} as any satisfies i18n } as any satisfies i18n

View File

@@ -488,4 +488,8 @@ export default {
485: 'Interfejs StartOS', 485: 'Interfejs StartOS',
486: 'WiFi', 486: 'WiFi',
487: 'instrukcje', 487: 'instrukcje',
488: 'hiszpański',
489: 'polski',
490: 'niemiecki',
491: 'angielski',
} satisfies i18n } satisfies i18n

View File

@@ -1,5 +1,5 @@
import { inject, Injectable, Pipe, PipeTransform } from '@angular/core' import { inject, Injectable, Pipe, PipeTransform } from '@angular/core'
import { ENGLISH } from './dictionaries/english' import { ENGLISH } from './dictionaries/en'
import { I18N, i18nKey } from './i18n.providers' import { I18N, i18nKey } from './i18n.providers'
@Pipe({ @Pipe({

View File

@@ -5,7 +5,7 @@ import {
tuiLanguageSwitcher, tuiLanguageSwitcher,
TuiLanguageSwitcherService, TuiLanguageSwitcherService,
} from '@taiga-ui/i18n' } from '@taiga-ui/i18n'
import { ENGLISH } from './dictionaries/english' import { ENGLISH } from './dictionaries/en'
import { i18nService } from './i18n.service' import { i18nService } from './i18n.service'
export type i18nKey = keyof typeof ENGLISH export type i18nKey = keyof typeof ENGLISH
@@ -36,11 +36,11 @@ export const I18N_PROVIDERS = [
useValue: async (language: TuiLanguageName): Promise<unknown> => { useValue: async (language: TuiLanguageName): Promise<unknown> => {
switch (language) { switch (language) {
case 'spanish': case 'spanish':
return import('./dictionaries/spanish').then(v => v.default) return import('./dictionaries/es').then(v => v.default)
case 'polish': case 'polish':
return import('./dictionaries/polish').then(v => v.default) return import('./dictionaries/pl').then(v => v.default)
case 'german': case 'german':
return import('./dictionaries/german').then(v => v.default) return import('./dictionaries/de').then(v => v.default)
default: default:
return null return null
} }

View File

@@ -2,7 +2,6 @@ import { ErrorHandler, inject, Injectable } from '@angular/core'
import { TuiAlertService } from '@taiga-ui/core' import { TuiAlertService } from '@taiga-ui/core'
import { HttpError } from '../classes/http-error' import { HttpError } from '../classes/http-error'
// @TODO Alex: Enable this as ErrorHandler
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })

View File

@@ -134,11 +134,6 @@ tui-dropdown[data-appearance='start-os'][data-appearance='start-os'] {
} }
} }
[tuiSidebar] > div.t-wrapper {
backdrop-filter: blur(1rem);
background: rgb(34 34 34 / 80%);
}
// @TODO Alex: Move to Taiga UI // @TODO Alex: Move to Taiga UI
a[tuiIconButton]:not([href]) { a[tuiIconButton]:not([href]) {
pointer-events: none; pointer-events: none;

View File

@@ -1,5 +0,0 @@
<tui-root tuiTheme="dark" [class.offline]="offline$ | async">
<router-outlet />
<toast-container />
<sidebar-host ngProjectAs="tuiOverContent" />
</tui-root>

View File

@@ -1,16 +0,0 @@
@import '@taiga-ui/core/styles/taiga-ui-local';
:host {
display: block;
height: 100%;
}
tui-root {
@include transition(filter);
height: 100%;
font-family: 'Open Sans', sans-serif;
&.offline {
filter: saturate(0.75) contrast(0.85);
}
}

View File

@@ -1,23 +1,36 @@
import { Component, inject, OnInit } from '@angular/core' import { Component, inject } from '@angular/core'
import { takeUntilDestroyed } from '@angular/core/rxjs-interop' import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
import { Title } from '@angular/platform-browser' import { Title } from '@angular/platform-browser'
import { i18nService } from '@start9labs/shared' import { i18nService } from '@start9labs/shared'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { combineLatest, map, merge, startWith } from 'rxjs' import { merge } from 'rxjs'
import { ConnectionService } from './services/connection.service'
import { PatchDataService } from './services/patch-data.service' import { PatchDataService } from './services/patch-data.service'
import { DataModel } from './services/patch-db/data-model' import { DataModel } from './services/patch-db/data-model'
import { PatchMonitorService } from './services/patch-monitor.service' import { PatchMonitorService } from './services/patch-monitor.service'
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
templateUrl: 'app.component.html', template: `
styleUrls: ['app.component.scss'], <tui-root tuiTheme="dark">
<router-outlet />
<toast-container />
</tui-root>
`,
styles: `
:host {
display: block;
height: 100%;
}
tui-root {
height: 100%;
font-family: 'Open Sans', sans-serif;
}
`,
}) })
export class AppComponent implements OnInit { export class AppComponent {
private readonly title = inject(Title) private readonly title = inject(Title)
private readonly i18n = inject(i18nService) private readonly i18n = inject(i18nService)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
readonly subscription = merge( readonly subscription = merge(
inject(PatchDataService), inject(PatchDataService),
@@ -26,23 +39,11 @@ export class AppComponent implements OnInit {
.pipe(takeUntilDestroyed()) .pipe(takeUntilDestroyed())
.subscribe() .subscribe()
readonly offline$ = combineLatest([ readonly ui = inject<PatchDB<DataModel>>(PatchDB)
inject(ConnectionService), .watch$('ui')
this.patch .pipe(takeUntilDestroyed())
.watch$('serverInfo', 'statusInfo') .subscribe(({ name, language }) => {
.pipe(startWith({ restarting: false, shuttingDown: false })),
]).pipe(
map(
([connected, { restarting, shuttingDown }]) =>
connected && (restarting || shuttingDown),
),
startWith(true),
)
ngOnInit() {
this.patch.watch$('ui').subscribe(({ name, language }) => {
this.title.setTitle(name || 'StartOS') this.title.setTitle(name || 'StartOS')
this.i18n.setLanguage(language || 'english') this.i18n.setLanguage(language || 'english')
}) })
}
} }

View File

@@ -3,7 +3,6 @@ import { NgModule } from '@angular/core'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { ServiceWorkerModule } from '@angular/service-worker' import { ServiceWorkerModule } from '@angular/service-worker'
import { TuiRoot } from '@taiga-ui/core' import { TuiRoot } from '@taiga-ui/core'
import { SidebarHostComponent } from 'src/app/components/sidebar-host.component'
import { ToastContainerComponent } from 'src/app/components/toast-container.component' import { ToastContainerComponent } from 'src/app/components/toast-container.component'
import { environment } from '../environments/environment' import { environment } from '../environments/environment'
import { AppComponent } from './app.component' import { AppComponent } from './app.component'
@@ -24,7 +23,6 @@ import { RoutingModule } from './routing.module'
// or after 30 seconds (whichever comes first). // or after 30 seconds (whichever comes first).
registrationStrategy: 'registerWhenStable:30000', registrationStrategy: 'registerWhenStable:30000',
}), }),
SidebarHostComponent,
], ],
providers: APP_PROVIDERS, providers: APP_PROVIDERS,
bootstrap: [AppComponent], bootstrap: [AppComponent],

View File

@@ -19,7 +19,7 @@ import {
tuiDropdownOptionsProvider, tuiDropdownOptionsProvider,
tuiNumberFormatProvider, tuiNumberFormatProvider,
} from '@taiga-ui/core' } from '@taiga-ui/core'
import { NG_EVENT_PLUGINS } from '@taiga-ui/event-plugins' import { provideEventPlugins } from '@taiga-ui/event-plugins'
import { import {
TUI_DATE_TIME_VALUE_TRANSFORMER, TUI_DATE_TIME_VALUE_TRANSFORMER,
TUI_DATE_VALUE_TRANSFORMER, TUI_DATE_VALUE_TRANSFORMER,
@@ -48,7 +48,7 @@ const {
} = require('../../../../config.json') as WorkspaceConfig } = require('../../../../config.json') as WorkspaceConfig
export const APP_PROVIDERS: Provider[] = [ export const APP_PROVIDERS: Provider[] = [
NG_EVENT_PLUGINS, provideEventPlugins(),
I18N_PROVIDERS, I18N_PROVIDERS,
FilterPackagesPipe, FilterPackagesPipe,
UntypedFormBuilder, UntypedFormBuilder,

View File

@@ -1,73 +1,94 @@
import { AsyncPipe } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import { SwUpdate } from '@angular/service-worker' import { SwUpdate } from '@angular/service-worker'
import { Exver, LoadingService } from '@start9labs/shared' import { WA_WINDOW } from '@ng-web-apis/common'
import { LoadingService } from '@start9labs/shared'
import { Version } from '@start9labs/start-sdk'
import { TuiResponsiveDialog } from '@taiga-ui/addon-mobile'
import { TuiAutoFocus } from '@taiga-ui/cdk' import { TuiAutoFocus } from '@taiga-ui/cdk'
import { TuiButton, TuiDialog } from '@taiga-ui/core' import { TuiButton } from '@taiga-ui/core'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { debounceTime, endWith, map, merge, Subject } from 'rxjs' import { distinctUntilChanged, map, merge, Subject } from 'rxjs'
import { ConfigService } from 'src/app/services/config.service' import { ConfigService } from 'src/app/services/config.service'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
// @TODO Alex
@Component({ @Component({
standalone: true, standalone: true,
selector: 'refresh-alert', selector: 'refresh-alert',
template: ` template: `
<!-- <ng-template--> <ng-template
<!-- [tuiDialog]="show$ | async"--> [tuiResponsiveDialog]="show()"
<!-- [tuiDialogOptions]="{ label: 'Refresh Needed', size: 's' }"--> [tuiResponsiveDialogOptions]="{ label: 'Refresh Needed', size: 's' }"
<!-- (tuiDialogChange)="onDismiss()"--> (tuiResponsiveDialogChange)="dismiss$.next()"
<!-- >--> >
<!-- Your user interface is cached and out of date. Hard refresh the page to--> @if (isPwa) {
<!-- get the latest UI.--> <p>
<!-- <ul>--> Your user interface is cached and out of date. Attempt to reload the
<!-- <li>--> PWA using the button below. If you continue to see this message,
<!-- <b>On Mac</b>--> uninstall and reinstall the PWA.
<!-- : cmd + shift + R--> </p>
<!-- </li>--> <button
<!-- <li>--> tuiButton
<!-- <b>On Linux/Windows</b>--> tuiAutoFocus
<!-- : ctrl + shift + R--> appearance="secondary"
<!-- </li>--> style="float: right"
<!-- </ul>--> [tuiAppearanceFocus]="false"
<!-- <button--> (click)="pwaReload()"
<!-- tuiButton--> >
<!-- tuiAutoFocus--> Reload
<!-- appearance="secondary"--> </button>
<!-- style="float: right"--> } @else {
<!-- (click)="onDismiss()"--> Your user interface is cached and out of date. Hard refresh the page to
<!-- >--> get the latest UI.
<!-- Ok--> <ul>
<!-- </button>--> <li>
<!-- </ng-template>--> <b>On Mac</b>
: cmd + shift + R
</li>
<li>
<b>On Linux/Windows</b>
: ctrl + shift + R
</li>
</ul>
<button
tuiButton
tuiAutoFocus
appearance="secondary"
style="float: right"
[tuiAppearanceFocus]="false"
(click)="dismiss$.next()"
>
Ok
</button>
}
</ng-template>
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiDialog, AsyncPipe, TuiButton, TuiAutoFocus], imports: [TuiResponsiveDialog, TuiButton, TuiAutoFocus],
}) })
export class RefreshAlertComponent { export class RefreshAlertComponent {
private readonly win = inject(WA_WINDOW)
private readonly updates = inject(SwUpdate) private readonly updates = inject(SwUpdate)
private readonly loader = inject(LoadingService) private readonly loader = inject(LoadingService)
private readonly exver = inject(Exver) private readonly version = Version.parse(inject(ConfigService).version)
private readonly config = inject(ConfigService)
private readonly dismiss$ = new Subject<boolean>()
readonly show$ = merge( readonly dismiss$ = new Subject<void>()
this.dismiss$, readonly isPwa = this.win.matchMedia('(display-mode: standalone)').matches
inject<PatchDB<DataModel>>(PatchDB)
.watch$('serverInfo', 'version')
.pipe(
map(version => !!this.exver.compareExver(this.config.version, version)),
endWith(false),
),
).pipe(debounceTime(0))
// @TODO use this like we did on 0344 readonly show = toSignal(
onPwa = false merge(
this.dismiss$.pipe(map(() => false)),
ngOnInit() { inject<PatchDB<DataModel>>(PatchDB)
this.onPwa = window.matchMedia('(display-mode: standalone)').matches .watch$('serverInfo', 'version')
} .pipe(
distinctUntilChanged(),
map(v => this.version.compare(Version.parse(v)) !== 'equal'),
),
),
{
initialValue: false,
},
)
async pwaReload() { async pwaReload() {
const loader = this.loader.open('Reloading PWA').subscribe() const loader = this.loader.open('Reloading PWA').subscribe()
@@ -80,11 +101,7 @@ export class RefreshAlertComponent {
} finally { } finally {
loader.unsubscribe() loader.unsubscribe()
// always reload, as this resolves most out of sync cases // always reload, as this resolves most out of sync cases
window.location.reload() this.win.location.reload()
} }
} }
onDismiss() {
this.dismiss$.next(false)
}
} }

View File

@@ -1,28 +0,0 @@
import { TuiDropdownService } from '@taiga-ui/core'
import {
ChangeDetectionStrategy,
Component,
Directive,
Injectable,
} from '@angular/core'
import { TuiPortals, TuiPortalService } from '@taiga-ui/cdk'
@Injectable({ providedIn: `root` })
export class SidebarService extends TuiPortalService {}
@Directive({
selector: '[tuiSidebar]',
standalone: true,
providers: [{ provide: TuiDropdownService, useExisting: SidebarService }],
})
export class SidebarDirective {}
@Component({
selector: 'sidebar-host',
template: '<ng-container #viewContainer></ng-container>',
styles: [':host { position: fixed; top: 0; }'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
providers: [{ provide: TuiPortalService, useExisting: SidebarService }],
})
export class SidebarHostComponent extends TuiPortals {}

View File

@@ -9,8 +9,7 @@ const ROUTES: Routes = [
}, },
{ {
path: 'logs', path: 'logs',
loadChildren: () => loadComponent: () => import('./logs.component'),
import('./logs/logs.module').then(m => m.LogsPageModule),
}, },
] ]

View File

@@ -1,12 +1,43 @@
import { Component, ElementRef, inject, OnInit, ViewChild } from '@angular/core' import { Component, ElementRef, inject, OnInit, ViewChild } from '@angular/core'
import { INTERSECTION_ROOT } from '@ng-web-apis/intersection-observer' import { RouterLink } from '@angular/router'
import {
WA_INTERSECTION_ROOT,
WaIntersectionObserver,
} from '@ng-web-apis/intersection-observer'
import { WaMutationObserver } from '@ng-web-apis/mutation-observer'
import { convertAnsi, ErrorService } from '@start9labs/shared' import { convertAnsi, ErrorService } from '@start9labs/shared'
import { TuiScrollbar } from '@taiga-ui/core' import { tuiProvide } from '@taiga-ui/cdk'
import { TuiButton, TuiLoader, TuiScrollbar } from '@taiga-ui/core'
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
@Component({ @Component({
selector: 'logs', standalone: true,
templateUrl: './logs.page.html', template: `
<a
routerLink="../"
tuiButton
iconStart="@tui.chevron-left"
appearance="icon"
[style.align-self]="'flex-start'"
>
Back
</a>
<tui-scrollbar childList subtree (waMutationObserver)="restoreScroll()">
<section
class="top"
waIntersectionObserver
(waIntersectionObservee)="onTop(!!$event[0]?.isIntersecting)"
>
@if (loading) {
<tui-loader textContent="Loading logs" />
}
</section>
@for (log of logs; track log) {
<pre [innerHTML]="log | dompurify"></pre>
}
</tui-scrollbar>
`,
styles: ` styles: `
:host { :host {
max-height: 100vh; max-height: 100vh;
@@ -18,14 +49,18 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
background: var(--tui-background-base); background: var(--tui-background-base);
} }
`, `,
providers: [ imports: [
{ RouterLink,
provide: INTERSECTION_ROOT, WaIntersectionObserver,
useExisting: ElementRef, WaMutationObserver,
}, NgDompurifyModule,
TuiButton,
TuiLoader,
TuiScrollbar,
], ],
providers: [tuiProvide(WA_INTERSECTION_ROOT, ElementRef)],
}) })
export class LogsPage implements OnInit { export default class LogsPage implements OnInit {
@ViewChild(TuiScrollbar, { read: ElementRef }) @ViewChild(TuiScrollbar, { read: ElementRef })
private readonly scrollbar?: ElementRef<HTMLElement> private readonly scrollbar?: ElementRef<HTMLElement>
private readonly api = inject(ApiService) private readonly api = inject(ApiService)

View File

@@ -1,32 +0,0 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { WaIntersectionObserver } from '@ng-web-apis/intersection-observer'
import { WaMutationObserver } from '@ng-web-apis/mutation-observer'
import { TuiButton, TuiLoader, TuiScrollbar } from '@taiga-ui/core'
import { TuiBadge } from '@taiga-ui/kit'
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
import { LogsPage } from './logs.page'
const ROUTES: Routes = [
{
path: '',
component: LogsPage,
},
]
@NgModule({
imports: [
CommonModule,
RouterModule.forChild(ROUTES),
...WaIntersectionObserver,
WaMutationObserver,
NgDompurifyModule,
TuiBadge,
TuiButton,
TuiLoader,
TuiScrollbar,
],
declarations: [LogsPage],
})
export class LogsPageModule {}

View File

@@ -1,23 +0,0 @@
<a
routerLink="../"
tuiButton
iconStart="@tui.chevron-left"
appearance="icon"
[style.align-self]="'flex-start'"
>
Back
</a>
<tui-scrollbar childList subtree (waMutationObserver)="restoreScroll()">
<section
class="top"
waIntersectionObserver
(waIntersectionObservee)="onTop(!!$event[0]?.isIntersecting)"
>
@if (loading) {
<tui-loader textContent="Loading logs" />
}
</section>
@for (log of logs; track log) {
<pre [innerHTML]="log | dompurify"></pre>
}
</tui-scrollbar>

View File

@@ -80,7 +80,7 @@ export interface FormContext<T> {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
padding: 1rem 0; padding: 1rem 0;
margin: 1rem 0 -1rem; margin: 1rem -1px -1rem;
gap: 1rem; gap: 1rem;
background: var(--tui-background-elevation-1); background: var(--tui-background-elevation-1);
border-top: 1px solid var(--tui-background-base-alt); border-top: 1px solid var(--tui-background-base-alt);

View File

@@ -17,7 +17,6 @@ export abstract class Control<
return this.control.spec return this.control.spec
} }
// @TODO Alex: Properly handle already set immutable value
get readOnly(): boolean { get readOnly(): boolean {
return ( return (
!!this.value && !!this.control.control?.pristine && this.control.immutable !!this.value && !!this.control.control?.pristine && this.control.immutable

View File

@@ -5,7 +5,7 @@ import { Control } from '../control'
@Component({ @Component({
selector: 'form-toggle', selector: 'form-toggle',
templateUrl: './form-toggle.component.html', templateUrl: './form-toggle.component.html',
host: { style: 'display: flex' }, host: { class: 'g-toggle' },
}) })
export class FormToggleComponent extends Control< export class FormToggleComponent extends Control<
IST.ValueSpecToggle, IST.ValueSpecToggle,

View File

@@ -24,27 +24,28 @@ import { tuiPure } from '@taiga-ui/cdk'
}) })
export class FormUnionComponent implements OnChanges { export class FormUnionComponent implements OnChanges {
@Input({ required: true }) @Input({ required: true })
spec!: IST.ValueSpecUnion spec!: IST.ValueSpecUnion & { others?: Record<string, any> }
selectSpec!: IST.ValueSpecSelect selectSpec!: IST.ValueSpecSelect
private readonly form = inject(FormGroupName) private readonly form = inject(FormGroupName)
private readonly formService = inject(FormService) private readonly formService = inject(FormService)
private readonly values: Record<string, any> = {}
get union(): string { get union(): string {
return this.form.value.selection return this.form.value.selection
} }
// OTHER?
@tuiPure @tuiPure
onUnion(union: string) { onUnion(union: string) {
this.values[this.union] = this.form.control.controls['value']?.value this.spec.others = this.spec.others || {}
this.spec.others[this.union] = this.form.control.controls['value']?.value
this.form.control.setControl( this.form.control.setControl(
'value', 'value',
this.formService.getFormGroup( this.formService.getFormGroup(
union ? this.spec.variants[union]?.spec || {} : {}, union ? this.spec.variants[union]?.spec || {} : {},
[], [],
this.values[union], this.spec.others[union],
), ),
{ {
emitEvent: false, emitEvent: false,

View File

@@ -31,6 +31,9 @@ import { HeaderStatusComponent } from './status.component'
border-radius: var(--bumper); border-radius: var(--bumper);
margin: var(--bumper); margin: var(--bumper);
overflow: hidden; overflow: hidden;
filter: grayscale(1) brightness(0.75);
@include transition(filter);
.mobile { .mobile {
display: none; display: none;
@@ -88,10 +91,12 @@ import { HeaderStatusComponent } from './status.component'
&:has([data-status='neutral']) { &:has([data-status='neutral']) {
--status: var(--tui-status-neutral); --status: var(--tui-status-neutral);
filter: none;
} }
&:has([data-status='success']) { &:has([data-status='success']) {
--status: transparent; --status: transparent;
filter: none;
} }
} }

View File

@@ -25,6 +25,7 @@ import { getMenu } from 'src/app/utils/system-utilities'
tuiHintDirection="bottom" tuiHintDirection="bottom"
[tuiHintShowDelay]="1000" [tuiHintShowDelay]="1000"
[routerLink]="item.routerLink" [routerLink]="item.routerLink"
[class.link_system]="item.routerLink === '/portal/system'"
[tuiHint]="!rla.isActive ? item.name : ''" [tuiHint]="!rla.isActive ? item.name : ''"
> >
<tui-badged-content <tui-badged-content
@@ -121,6 +122,10 @@ import { getMenu } from 'src/app/utils/system-utilities'
padding: 0 1rem; padding: 0 1rem;
margin: 0 calc(var(--bumper) + 0.5rem); margin: 0 calc(var(--bumper) + 0.5rem);
&.link_system {
pointer-events: none;
}
+ .link::before { + .link::before {
left: -0.5rem; left: -0.5rem;
border-top-left-radius: var(--bumper); border-top-left-radius: var(--bumper);

View File

@@ -33,7 +33,7 @@
} }
</section> </section>
} @else { } @else {
<tui-loader [textContent]="'Loading logs' | i18n" [style.margin-top.rem]="5" /> <tui-loader class="loader" [textContent]="'Loading logs' | i18n" />
} }
<section <section
@@ -53,7 +53,7 @@
iconStart="@tui.circle-arrow-down" iconStart="@tui.circle-arrow-down"
(click)="setScroll(true); scrollToBottom()" (click)="setScroll(true); scrollToBottom()"
> >
{{'Scroll to bottom' | i18n}} {{ 'Scroll to bottom' | i18n }}
</button> </button>
<button <button
tuiButton tuiButton
@@ -61,6 +61,6 @@
iconStart="@tui.download" iconStart="@tui.download"
[logsDownload]="fetchLogs" [logsDownload]="fetchLogs"
> >
{{'Download' | i18n}} {{ 'Download' | i18n }}
</button> </button>
</footer> </footer>

View File

@@ -21,6 +21,11 @@
margin-bottom: -5rem; margin-bottom: -5rem;
} }
.loader {
position: absolute;
inset: 0;
}
.bottom { .bottom {
height: 3rem; height: 3rem;
} }

View File

@@ -20,6 +20,8 @@ import { HeaderComponent } from './components/header/header.component'
`, `,
styles: [ styles: [
` `
@import '@taiga-ui/core/styles/taiga-ui-local';
:host { :host {
height: 100%; height: 100%;
display: flex; display: flex;
@@ -32,6 +34,14 @@ import { HeaderComponent } from './components/header/header.component'
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
margin: 0 var(--bumper) var(--bumper); margin: 0 var(--bumper) var(--bumper);
filter: grayscale(1) brightness(0.75);
@include transition(filter);
header:has([data-status='success']) + &,
header:has([data-status='neutral']) + & {
filter: none;
}
} }
`, `,
], ],

View File

@@ -4,7 +4,6 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
inject, inject,
input,
Input, Input,
} from '@angular/core' } from '@angular/core'
import { Router } from '@angular/router' import { Router } from '@angular/router'
@@ -76,7 +75,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
<button <button
tuiButton tuiButton
type="button" type="button"
appearance="outline-grayscale" appearance="secondary-grayscale"
(click)="showService()" (click)="showService()"
> >
{{ 'View Installed' | i18n }} {{ 'View Installed' | i18n }}
@@ -180,8 +179,9 @@ export class MarketplaceControlsComponent {
private async installOrUpload(url: string | null) { private async installOrUpload(url: string | null) {
if (this.file) { if (this.file) {
await this.upload() await this.upload()
this.router.navigate(['/portal', 'services'])
} else if (url) { } else if (url) {
this.install(url) await this.install(url)
} }
} }

View File

@@ -1,26 +0,0 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { TuiPortals, TuiPortalService } from '@taiga-ui/cdk'
import { MarketplaceSidebarService } from '../services/sidebar.service'
@Component({
standalone: true,
selector: 'marketplace-sidebars',
template: '<ng-container #viewContainer></ng-container>',
styles: [
`
:host {
position: fixed;
inset: 3.5rem 0 0;
pointer-events: none;
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: TuiPortalService,
useExisting: MarketplaceSidebarService,
},
],
})
export class MarketplaceSidebarsComponent extends TuiPortals {}

View File

@@ -10,9 +10,9 @@ import { toObservable, toSignal } from '@angular/core/rxjs-interop'
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
import { ItemModule, MarketplacePkg } from '@start9labs/marketplace' import { ItemModule, MarketplacePkg } from '@start9labs/marketplace'
import { Exver } from '@start9labs/shared' import { Exver } from '@start9labs/shared'
import { TuiSidebar } from '@taiga-ui/addon-mobile' import { TuiAutoFocus } from '@taiga-ui/cdk'
import { TuiAutoFocus, TuiClickOutside } from '@taiga-ui/cdk' import { TuiButton, TuiDropdownService, TuiPopup } from '@taiga-ui/core'
import { TuiButton, TuiDropdownService } from '@taiga-ui/core' import { TuiDrawer } from '@taiga-ui/kit'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { import {
debounceTime, debounceTime,
@@ -35,40 +35,48 @@ import { MarketplaceControlsComponent } from './controls.component'
selector: 'marketplace-tile', selector: 'marketplace-tile',
template: ` template: `
<marketplace-item [pkg]="pkg()" (click)="toggle(true)"> <marketplace-item [pkg]="pkg()" (click)="toggle(true)">
<marketplace-preview <tui-drawer
*tuiSidebar="!!open(); direction: 'right'; autoWidth: true" *tuiPopup="open()"
[pkgId]="pkg().id" [overlay]="true"
class="preview-wrapper" (click.self)="toggle(false)"
(tuiClickOutside)="toggle(false)"
> >
<button <marketplace-preview [pkgId]="pkg().id" class="preview-wrapper">
tuiAutoFocus <button
slot="close" tuiAutoFocus
size="xs" slot="close"
class="close-button" size="xs"
tuiIconButton class="close-button"
type="button" tuiIconButton
appearance="icon" type="button"
iconStart="@tui.x" appearance="icon"
[tuiAppearanceFocus]="false" iconStart="@tui.x"
(click)="toggle(false)" [tuiAppearanceFocus]="false"
></button> (click)="toggle(false)"
<marketplace-controls ></button>
slot="controls" <marketplace-controls
class="controls-wrapper" slot="controls"
[pkg]="pkg()" class="controls-wrapper"
[localPkg]="local$ | async" [pkg]="pkg()"
[localFlavor]="!!(flavor$ | async)" [localPkg]="local$ | async"
/> [localFlavor]="!!(flavor$ | async)"
</marketplace-preview> />
</marketplace-preview>
</tui-drawer>
</marketplace-item> </marketplace-item>
`, `,
styles: [ styles: [
` `
:host { :host {
cursor: pointer;
animation: animateIn 400ms calc(var(--animation-order) * 200ms) both; animation: animateIn 400ms calc(var(--animation-order) * 200ms) both;
} }
tui-drawer {
top: 0;
width: 28rem;
border-radius: 0;
}
@keyframes animateIn { @keyframes animateIn {
from { from {
opacity: 0; opacity: 0;
@@ -119,9 +127,9 @@ import { MarketplaceControlsComponent } from './controls.component'
CommonModule, CommonModule,
ItemModule, ItemModule,
TuiAutoFocus, TuiAutoFocus,
TuiClickOutside,
TuiSidebar,
TuiButton, TuiButton,
TuiPopup,
TuiDrawer,
MarketplaceControlsComponent, MarketplaceControlsComponent,
MarketplacePreviewComponent, MarketplacePreviewComponent,
], ],

View File

@@ -7,6 +7,7 @@ import {
FilterPackagesPipe, FilterPackagesPipe,
FilterPackagesPipeModule, FilterPackagesPipeModule,
} from '@start9labs/marketplace' } from '@start9labs/marketplace'
import { i18nPipe } from '@start9labs/shared'
import { TuiScrollbar } from '@taiga-ui/core' import { TuiScrollbar } from '@taiga-ui/core'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { tap, withLatestFrom } from 'rxjs' import { tap, withLatestFrom } from 'rxjs'
@@ -15,9 +16,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
import { TitleDirective } from 'src/app/services/title.service' import { TitleDirective } from 'src/app/services/title.service'
import { MarketplaceMenuComponent } from './components/menu.component' import { MarketplaceMenuComponent } from './components/menu.component'
import { MarketplaceNotificationComponent } from './components/notification.component' import { MarketplaceNotificationComponent } from './components/notification.component'
import { MarketplaceSidebarsComponent } from './components/sidebars.component'
import { MarketplaceTileComponent } from './components/tile.component' import { MarketplaceTileComponent } from './components/tile.component'
import { i18nPipe } from '@start9labs/shared'
@Component({ @Component({
standalone: true, standalone: true,
@@ -56,7 +55,6 @@ import { i18nPipe } from '@start9labs/shared'
</div> </div>
</div> </div>
</tui-scrollbar> </tui-scrollbar>
<marketplace-sidebars />
`, `,
host: { class: 'g-page' }, host: { class: 'g-page' },
styles: [ styles: [
@@ -153,7 +151,6 @@ import { i18nPipe } from '@start9labs/shared'
MarketplaceTileComponent, MarketplaceTileComponent,
MarketplaceMenuComponent, MarketplaceMenuComponent,
MarketplaceNotificationComponent, MarketplaceNotificationComponent,
MarketplaceSidebarsComponent,
TuiScrollbar, TuiScrollbar,
FilterPackagesPipeModule, FilterPackagesPipeModule,
TitleDirective, TitleDirective,

View File

@@ -21,6 +21,7 @@ import {
DialogService, DialogService,
Exver, Exver,
i18nPipe, i18nPipe,
MARKDOWN,
SharedPipesModule, SharedPipesModule,
} from '@start9labs/shared' } from '@start9labs/shared'
import { TuiButton, TuiDialogContext, TuiLoader } from '@taiga-ui/core' import { TuiButton, TuiDialogContext, TuiLoader } from '@taiga-ui/core'
@@ -105,14 +106,7 @@ import { MarketplaceService } from 'src/app/services/marketplace.service'
.outer-container { .outer-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 1.75rem;
min-width: 100%;
height: calc(100vh - var(--portal-header-height) - var(--bumper)); height: calc(100vh - var(--portal-header-height) - var(--bumper));
margin-top: 5rem;
@media (min-width: 768px) {
margin-top: 0;
}
} }
.inner-container { .inner-container {
@@ -226,8 +220,21 @@ export class MarketplacePreviewComponent {
this.router.navigate([], { queryParams: { id } }) this.router.navigate([], { queryParams: { id } })
} }
onStatic(type: 'license' | 'instructions') { onStatic(asset: 'license' | 'instructions') {
// @TODO Alex need to display License or Instructions. This requires an API request, check out next/minor const label = asset === 'license' ? 'License' : 'Instructions'
const content = this.pkg$.pipe(
filter(Boolean),
switchMap(pkg =>
this.marketplaceService.fetchStatic$(
pkg,
asset === 'license' ? 'LICENSE.md' : 'instructions.md',
),
),
)
this.dialog
.openComponent(MARKDOWN, { label, size: 'l', data: { content } })
.subscribe()
} }
selectVersion( selectVersion(

View File

@@ -64,24 +64,25 @@ export class MarketplaceAlertsService {
} }
async alertInstall({ alerts }: MarketplacePkgBase): Promise<boolean> { async alertInstall({ alerts }: MarketplacePkgBase): Promise<boolean> {
const content = alerts.install as i18nKey const content = alerts.install
return ( return (
!!content && !content ||
new Promise(resolve => { (!!content &&
this.dialog new Promise(resolve => {
.openConfirm<boolean>({ this.dialog
label: 'Alert', .openConfirm<boolean>({
size: 's', label: 'Alert',
data: { size: 's',
content, data: {
yes: 'Install', content: content as i18nKey,
no: 'Cancel', yes: 'Install',
}, no: 'Cancel',
}) },
.pipe(defaultIfEmpty(false)) })
.subscribe(response => resolve(response)) .pipe(defaultIfEmpty(false))
}) .subscribe(response => resolve(response))
}))
) )
} }
} }

View File

@@ -57,8 +57,6 @@ export class ServiceActionRequestsComponent {
readonly requests = computed(() => readonly requests = computed(() =>
Object.values(this.pkg().requestedActions) Object.values(this.pkg().requestedActions)
// @TODO Alex uncomment filter line below to produce infinite loop on service details page when dependency not installed. This means the page is infinitely trying to re-render
// .filter(r => r.active)
.filter( .filter(
r => r =>
this.services()[r.request.packageId]?.actions[r.request.actionId] && this.services()[r.request.packageId]?.actions[r.request.actionId] &&

View File

@@ -1,5 +1,6 @@
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { Component, inject, Input } from '@angular/core' import { Component, inject, input } from '@angular/core'
import { toObservable } from '@angular/core/rxjs-interop'
import { import {
AboutModule, AboutModule,
AdditionalModule, AdditionalModule,
@@ -12,11 +13,11 @@ import {
MARKDOWN, MARKDOWN,
SharedPipesModule, SharedPipesModule,
} from '@start9labs/shared' } from '@start9labs/shared'
import { MarketplaceControlsComponent } from '../marketplace/components/controls.component'
import { filter, first, map } from 'rxjs'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { filter, first, map, of, switchMap } from 'rxjs'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
import { getManifest } from 'src/app/utils/get-package-data' import { getManifest } from 'src/app/utils/get-package-data'
import { MarketplaceControlsComponent } from '../marketplace/components/controls.component'
import { MarketplacePkgSideload } from './sideload.utils' import { MarketplacePkgSideload } from './sideload.utils'
@Component({ @Component({
@@ -24,25 +25,25 @@ import { MarketplacePkgSideload } from './sideload.utils'
template: ` template: `
<div class="outer-container"> <div class="outer-container">
<ng-content /> <ng-content />
<marketplace-package-hero [pkg]="pkg"> <marketplace-package-hero [pkg]="pkg()">
<marketplace-controls <marketplace-controls
slot="controls" slot="controls"
class="controls-wrapper" class="controls-wrapper"
[pkg]="pkg" [pkg]="pkg()"
[localPkg]="local$ | async" [localPkg]="local$ | async"
[localFlavor]="!!(flavor$ | async)" [localFlavor]="!!(flavor$ | async)"
[file]="file" [file]="file()"
/> />
</marketplace-package-hero> </marketplace-package-hero>
<div class="package-details"> <div class="package-details">
<div class="package-details-main"> <div class="package-details-main">
<marketplace-about [pkg]="pkg" /> <marketplace-about [pkg]="pkg()" />
@if (!(pkg.dependencyMetadata | empty)) { @if (!(pkg().dependencyMetadata | empty)) {
<marketplace-dependencies [pkg]="pkg" /> <marketplace-dependencies [pkg]="pkg()" />
} }
</div> </div>
<div class="package-details-additional"> <div class="package-details-additional">
<marketplace-additional [pkg]="pkg" (static)="onStatic($event)" /> <marketplace-additional [pkg]="pkg()" (static)="onStatic($event)" />
</div> </div>
</div> </div>
</div> </div>
@@ -108,38 +109,35 @@ export class SideloadPackageComponent {
private readonly patch = inject<PatchDB<DataModel>>(PatchDB) private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
private readonly dialog = inject(DialogService) private readonly dialog = inject(DialogService)
// @Input({ required: true }) readonly pkg = input.required<MarketplacePkgSideload>()
// pkg!: MarketplacePkgSideload readonly file = input.required<File>()
// @TODO Alex why do I need to initialize pkg below? I would prefer to do the above, but it's not working readonly local$ = toObservable(this.pkg).pipe(
@Input({ required: true })
pkg: MarketplacePkgSideload = {} as MarketplacePkgSideload
@Input({ required: true })
file!: File
readonly local$ = this.patch.watch$('packageData', this.pkg.id).pipe(
filter(Boolean), filter(Boolean),
map(pkg => switchMap(({ id, flavor }) =>
this.exver.getFlavor(getManifest(pkg).version) === this.pkg.flavor this.patch.watch$('packageData', id).pipe(
? pkg filter(Boolean),
: null, map(pkg =>
this.exver.getFlavor(getManifest(pkg).version) === flavor
? pkg
: null,
),
),
), ),
first(), first(),
) )
readonly flavor$ = this.local$.pipe(map(pkg => !pkg)) readonly flavor$ = this.local$.pipe(map(pkg => !pkg))
// @TODO Alex, struggling to get this working. I don't understand how to use this markdown component, only one other example, and it's very different.
onStatic(type: 'license' | 'instructions') { onStatic(type: 'license' | 'instructions') {
const label = type === 'license' ? 'License' : 'Instructions'
const key = type === 'license' ? 'fullLicense' : 'instructions'
this.dialog this.dialog
.openComponent(MARKDOWN, { .openComponent(MARKDOWN, {
label: type === 'license' ? 'License' : 'Instructions', label,
size: 'l', size: 'l',
data: { data: { content: of(this.pkg()[key]) },
content:
this.pkg[type === 'license' ? 'fullLicense' : 'instructions'],
},
}) })
.subscribe() .subscribe()
} }

View File

@@ -27,6 +27,7 @@ import { i18nKey, i18nPipe } from '@start9labs/shared'
tuiIconButton tuiIconButton
appearance="neutral" appearance="neutral"
iconStart="@tui.x" iconStart="@tui.x"
size="s"
[style.border-radius.%]="100" [style.border-radius.%]="100"
[style.justify-self]="'end'" [style.justify-self]="'end'"
(click)="clear()" (click)="clear()"

View File

@@ -26,7 +26,7 @@ import {
CifsBackupTarget, CifsBackupTarget,
DiskBackupTarget, DiskBackupTarget,
} from 'src/app/services/api/api.types' } from 'src/app/services/api/api.types'
import { EOSService } from 'src/app/services/eos.service' import { OSService } from 'src/app/services/os.service'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
import { TitleDirective } from 'src/app/services/title.service' import { TitleDirective } from 'src/app/services/title.service'
import { BACKUP } from './backup.component' import { BACKUP } from './backup.component'
@@ -103,7 +103,7 @@ import { BACKUP_RESTORE } from './restore.component'
</tui-notification> </tui-notification>
} }
@if (type === 'create' && (eos.backingUp$ | async)) { @if (type === 'create' && (os.backingUp$ | async)) {
<section backupProgress></section> <section backupProgress></section>
} @else { } @else {
@if (service.loading()) { @if (service.loading()) {
@@ -163,7 +163,7 @@ export default class SystemBackupComponent implements OnInit {
readonly dialog = inject(DialogService) readonly dialog = inject(DialogService)
readonly type = inject(ActivatedRoute).snapshot.data['type'] readonly type = inject(ActivatedRoute).snapshot.data['type']
readonly service = inject(BackupService) readonly service = inject(BackupService)
readonly eos = inject(EOSService) readonly os = inject(OSService)
readonly server = toSignal( readonly server = toSignal(
inject<PatchDB<DataModel>>(PatchDB).watch$('serverInfo'), inject<PatchDB<DataModel>>(PatchDB).watch$('serverInfo'),
) )

View File

@@ -115,6 +115,11 @@ const ERROR =
width: 13rem; width: 13rem;
} }
td:last-child {
white-space: nowrap;
text-align: right;
}
[tuiButton] { [tuiButton] {
margin-inline-start: auto; margin-inline-start: auto;
} }

View File

@@ -9,15 +9,17 @@ import { toSignal } from '@angular/core/rxjs-interop'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { RouterLink } from '@angular/router' import { RouterLink } from '@angular/router'
import { import {
DialogService,
ErrorService, ErrorService,
i18nKey,
i18nPipe, i18nPipe,
i18nService, i18nService,
LoadingService,
DialogService,
languages, languages,
i18nKey, Languages,
LoadingService,
} from '@start9labs/shared' } from '@start9labs/shared'
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile' import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
import { TuiContext, TuiStringHandler } from '@taiga-ui/cdk'
import { import {
TuiAppearance, TuiAppearance,
TuiButton, TuiButton,
@@ -38,7 +40,7 @@ import { PatchDB } from 'patch-db-client'
import { filter } from 'rxjs' import { filter } from 'rxjs'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ConfigService } from 'src/app/services/config.service' import { ConfigService } from 'src/app/services/config.service'
import { EOSService } from 'src/app/services/eos.service' import { OSService } from 'src/app/services/os.service'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
import { TitleDirective } from 'src/app/services/title.service' import { TitleDirective } from 'src/app/services/title.service'
import { SnekDirective } from './snek.directive' import { SnekDirective } from './snek.directive'
@@ -72,13 +74,13 @@ import { SystemWipeComponent } from './wipe.component'
tuiButton tuiButton
appearance="accent" appearance="accent"
iconStart="@tui.refresh-cw" iconStart="@tui.refresh-cw"
[disabled]="eos.updatingOrBackingUp$ | async" [disabled]="os.updatingOrBackingUp$ | async"
(click)="onUpdate()" (click)="onUpdate()"
> >
@if (server.statusInfo.updated) { @if (server.statusInfo.updated) {
{{ 'Restart to apply' | i18n }} {{ 'Restart to apply' | i18n }}
} @else { } @else {
@if (eos.showUpdate$ | async) { @if (os.showUpdate$ | async) {
{{ 'Update' | i18n }} {{ 'Update' | i18n }}
} @else { } @else {
{{ 'Check for updates' | i18n }} {{ 'Check for updates' | i18n }}
@@ -98,7 +100,13 @@ import { SystemWipeComponent } from './wipe.component'
<tui-icon icon="@tui.languages" /> <tui-icon icon="@tui.languages" />
<span tuiTitle> <span tuiTitle>
<strong>{{ 'Language' | i18n }}</strong> <strong>{{ 'Language' | i18n }}</strong>
<span tuiSubtitle>{{ i18nService.language }}</span> <span tuiSubtitle>
@if (language; as lang) {
{{ lang | i18n }}
} @else {
{{ i18nService.language }}
}
</span>
</span> </span>
<button <button
tuiButtonSelect tuiButtonSelect
@@ -112,6 +120,7 @@ import { SystemWipeComponent } from './wipe.component'
*tuiTextfieldDropdown *tuiTextfieldDropdown
size="l" size="l"
[items]="languages" [items]="languages"
[itemContent]="translation"
/> />
</button> </button>
</div> </div>
@@ -219,24 +228,32 @@ export default class SystemGeneralComponent {
private readonly isTor = inject(ConfigService).isTor() private readonly isTor = inject(ConfigService).isTor()
private readonly document = inject(DOCUMENT) private readonly document = inject(DOCUMENT)
private readonly dialog = inject(DialogService) private readonly dialog = inject(DialogService)
private readonly i18n = inject(i18nPipe)
wipe = false wipe = false
count = 0 count = 0
readonly server = toSignal(this.patch.watch$('serverInfo')) readonly server = toSignal(this.patch.watch$('serverInfo'))
readonly name = toSignal(this.patch.watch$('ui', 'name')) readonly name = toSignal(this.patch.watch$('ui', 'name'))
readonly eos = inject(EOSService) readonly os = inject(OSService)
readonly i18nService = inject(i18nService) readonly i18nService = inject(i18nService)
readonly languages = languages readonly languages = languages
readonly translation: TuiStringHandler<TuiContext<Languages>> = ({
$implicit,
}) => this.i18n.transform($implicit)!
readonly score = toSignal( readonly score = toSignal(
this.patch.watch$('ui', 'gaming', 'snake', 'highScore'), this.patch.watch$('ui', 'gaming', 'snake', 'highScore'),
{ initialValue: 0 }, { initialValue: 0 },
) )
get language(): Languages | undefined {
return this.languages.find(lang => lang === this.i18nService.language)
}
onUpdate() { onUpdate() {
if (this.server()?.statusInfo.updated) { if (this.server()?.statusInfo.updated) {
this.restart() this.restart()
} else if (this.eos.updateAvailable$.value) { } else if (this.os.updateAvailable$.value) {
this.update() this.update()
} else { } else {
this.check() this.check()
@@ -334,9 +351,9 @@ export default class SystemGeneralComponent {
const loader = this.loader.open('Checking for updates').subscribe() const loader = this.loader.open('Checking for updates').subscribe()
try { try {
await this.eos.loadEos() await this.os.loadOS()
if (this.eos.updateAvailable$.value) { if (this.os.updateAvailable$.value) {
this.update() this.update()
} else { } else {
this.dialog this.dialog

View File

@@ -15,7 +15,7 @@ import {
import { TuiDialogContext, TuiScrollbar, TuiButton } from '@taiga-ui/core' import { TuiDialogContext, TuiScrollbar, TuiButton } from '@taiga-ui/core'
import { NgDompurifyModule } from '@tinkoff/ng-dompurify' import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { EOSService } from 'src/app/services/eos.service' import { OSService } from 'src/app/services/os.service'
@Component({ @Component({
template: ` template: `
@@ -47,7 +47,7 @@ import { EOSService } from 'src/app/services/eos.service'
], ],
}) })
export class SystemUpdateModal { export class SystemUpdateModal {
readonly versions = Object.entries(this.eosService.osUpdate?.releaseNotes!) readonly versions = Object.entries(this.os.osUpdate?.releaseNotes!)
.sort(([a], [b]) => a.localeCompare(b)) .sort(([a], [b]) => a.localeCompare(b))
.reverse() .reverse()
.map(([version, notes]) => ({ .map(([version, notes]) => ({
@@ -60,7 +60,7 @@ export class SystemUpdateModal {
private readonly loader: LoadingService, private readonly loader: LoadingService,
private readonly errorService: ErrorService, private readonly errorService: ErrorService,
private readonly embassyApi: ApiService, private readonly embassyApi: ApiService,
private readonly eosService: EOSService, private readonly os: OSService,
) {} ) {}
async update() { async update() {

View File

@@ -18,7 +18,6 @@ import {
import { TuiButton, TuiIcon, TuiLink, TuiTitle } from '@taiga-ui/core' import { TuiButton, TuiIcon, TuiLink, TuiTitle } from '@taiga-ui/core'
import { TuiExpand } from '@taiga-ui/experimental' import { TuiExpand } from '@taiga-ui/experimental'
import { import {
TUI_CONFIRM,
TuiAvatar, TuiAvatar,
TuiButtonLoading, TuiButtonLoading,
TuiChevron, TuiChevron,
@@ -73,6 +72,14 @@ import UpdatesComponent from './updates.component'
<td class="desktop">{{ item().s9pk.publishedAt | date }}</td> <td class="desktop">{{ item().s9pk.publishedAt | date }}</td>
<td> <td>
<div> <div>
<button
tuiIconButton
size="m"
appearance="icon"
[tuiChevron]="expanded()"
>
{{ 'Show more' | i18n }}
</button>
@if (local().stateInfo.state === 'updating') { @if (local().stateInfo.state === 'updating') {
<tui-progress-circle <tui-progress-circle
class="g-positive" class="g-positive"
@@ -94,14 +101,6 @@ import UpdatesComponent from './updates.component'
{{ error() ? ('Retry' | i18n) : ('Update' | i18n) }} {{ error() ? ('Retry' | i18n) : ('Update' | i18n) }}
</button> </button>
} }
<button
tuiIconButton
size="s"
appearance="icon"
[tuiChevron]="expanded()"
>
{{ 'Show more' | i18n }}
</button>
</div> </div>
</td> </td>
</tr> </tr>

View File

@@ -1850,6 +1850,11 @@ export namespace Mock {
listitems: ['192.168.1.1', '192.1681.23'], listitems: ['192.168.1.1', '192.1681.23'],
name: 'Matt', name: 'Matt',
}, },
other: {
external: {
'public-domain': 'test.com',
},
},
}, },
port: 20, port: 20,
rpcallowip: undefined, rpcallowip: undefined,

View File

@@ -1058,11 +1058,11 @@ export class MockApiService extends ApiService {
...Mock.LocalPkgs[params.id]!, ...Mock.LocalPkgs[params.id]!,
stateInfo: { stateInfo: {
// if installing // if installing
// state: 'installing', state: 'installing',
// if updating // if updating
state: 'updating', // state: 'updating',
manifest: mockPatchData.packageData[params.id]?.stateInfo.manifest!, // manifest: mockPatchData.packageData[params.id]?.stateInfo.manifest!,
// both // both
installingInfo: { installingInfo: {

View File

@@ -14,7 +14,7 @@ import {
switchMap, switchMap,
} from 'rxjs' } from 'rxjs'
import { ConnectionService } from 'src/app/services/connection.service' import { ConnectionService } from 'src/app/services/connection.service'
import { EOSService } from 'src/app/services/eos.service' import { OSService } from 'src/app/services/os.service'
import { MarketplaceService } from 'src/app/services/marketplace.service' import { MarketplaceService } from 'src/app/services/marketplace.service'
import { NotificationService } from 'src/app/services/notification.service' import { NotificationService } from 'src/app/services/notification.service'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
@@ -27,7 +27,7 @@ export class BadgeService {
private readonly notifications = inject(NotificationService) private readonly notifications = inject(NotificationService)
private readonly exver = inject(Exver) private readonly exver = inject(Exver)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB) private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
private readonly system$ = inject(EOSService).updateAvailable$.pipe( private readonly system$ = inject(OSService).updateAvailable$.pipe(
map(Number), map(Number),
) )
private readonly metrics$ = this.patch private readonly metrics$ = this.patch

View File

@@ -73,7 +73,7 @@ export class FormService {
UntypedFormGroup | UntypedFormArray | UntypedFormControl UntypedFormGroup | UntypedFormArray | UntypedFormControl
> = {} > = {}
Object.entries(config).map(([key, spec]) => { Object.entries(config).map(([key, spec]) => {
group[key] = this.getFormEntry(spec, current ? current[key] : undefined) group[key] = this.getFormEntry(spec, current?.[key])
}) })
return this.formBuilder.group(group, { validators }) return this.formBuilder.group(group, { validators })
} }

View File

@@ -194,7 +194,7 @@ export class MarketplaceService {
) )
} }
getStatic$( fetchStatic$(
pkg: MarketplacePkg, pkg: MarketplacePkg,
type: 'LICENSE.md' | 'instructions.md', type: 'LICENSE.md' | 'instructions.md',
): Observable<string> { ): Observable<string> {

View File

@@ -10,7 +10,7 @@ import { Version } from '@start9labs/start-sdk'
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class EOSService { export class OSService {
osUpdate?: OSUpdate osUpdate?: OSUpdate
updateAvailable$ = new BehaviorSubject<boolean>(false) updateAvailable$ = new BehaviorSubject<boolean>(false)
@@ -45,7 +45,7 @@ export class EOSService {
private readonly patch: PatchDB<DataModel>, private readonly patch: PatchDB<DataModel>,
) {} ) {}
async loadEos(): Promise<void> { async loadOS(): Promise<void> {
const { version, id } = await getServerInfo(this.patch) const { version, id } = await getServerInfo(this.patch)
this.osUpdate = await this.api.checkOSUpdate({ serverId: id }) this.osUpdate = await this.api.checkOSUpdate({ serverId: id })
const updateAvailable = const updateAvailable =

View File

@@ -1,41 +1,33 @@
import { Injectable } from '@angular/core' import { inject, Injectable } from '@angular/core'
import { Observable } from 'rxjs' import { Observable } from 'rxjs'
import { filter, map, share, switchMap } from 'rxjs/operators' import { filter, map, share, switchMap } from 'rxjs/operators'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
import { EOSService } from 'src/app/services/eos.service' import { OSService } from 'src/app/services/os.service'
import { ConnectionService } from 'src/app/services/connection.service' import { ConnectionService } from 'src/app/services/connection.service'
import { LocalStorageBootstrap } from './patch-db/local-storage-bootstrap' import { LocalStorageBootstrap } from './patch-db/local-storage-bootstrap'
// @TODO Alex this file has just become checking for StartOS updates. Maybe it can be removed/simplified. I'm not sure why getMarketplace$() line is commented out, I assume we are checking for service updates somewhere else?
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class PatchDataService extends Observable<void> { export class PatchDataService extends Observable<void> {
private readonly stream$ = this.connection$.pipe( private readonly patch: PatchDB<DataModel> = inject(PatchDB)
private readonly os = inject(OSService)
private readonly bootstrapper = inject(LocalStorageBootstrap)
private readonly stream$ = inject(ConnectionService).pipe(
filter(Boolean), filter(Boolean),
switchMap(() => this.patch.watch$()), switchMap(() => this.patch.watch$()),
map((cache, index) => { map((cache, index) => {
this.bootstrapper.update(cache) this.bootstrapper.update(cache)
if (index === 0) { if (index === 0) {
this.checkForUpdates() this.os.loadOS()
} }
}), }),
share(), share(),
) )
constructor( constructor() {
private readonly patch: PatchDB<DataModel>,
private readonly eosService: EOSService,
private readonly connection$: ConnectionService,
private readonly bootstrapper: LocalStorageBootstrap,
) {
super(subscriber => this.stream$.subscribe(subscriber)) super(subscriber => this.stream$.subscribe(subscriber))
} }
private checkForUpdates(): void {
this.eosService.loadEos()
// this.marketplaceService.getMarketplace$().pipe(take(1)).subscribe()
}
} }

View File

@@ -26,7 +26,7 @@ export const STATUS = new InjectionToken('', {
return CONNECTED return CONNECTED
}), }),
), ),
{ initialValue: CONNECTED }, { initialValue: CONNECTING },
), ),
}) })